diff --git a/.cspell/frigate-dictionary.txt b/.cspell/frigate-dictionary.txt index 6e66a4704..f2bcf417a 100644 --- a/.cspell/frigate-dictionary.txt +++ b/.cspell/frigate-dictionary.txt @@ -22,6 +22,7 @@ autotrack autotracked autotracker autotracking +backchannel balena Beelink BGRA @@ -191,6 +192,7 @@ ONVIF openai opencv openvino +overfitting OWASP paddleocr paho @@ -315,4 +317,4 @@ yolo yolonas yolox zeep -zerolatency \ No newline at end of file +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 000000000..35034069b --- /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/.github/DISCUSSION_TEMPLATE/beta-support.yml b/.github/DISCUSSION_TEMPLATE/beta-support.yml new file mode 100644 index 000000000..e342127a0 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/beta-support.yml @@ -0,0 +1,129 @@ +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://deploy-preview-19787--frigate-docs.netlify.app/ + - 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 page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1) + placeholder: "0.17.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 Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - 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/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml index a32ee5938..de870ac0f 100644 --- a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml +++ b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -6,6 +6,8 @@ body: value: | Use this form to submit a reproducible bug in Frigate or Frigate's UI. + **⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.** + 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.** diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..89acd8a9b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +- For Frigate NVR, never write strings in the frontend directly. Since the project uses `react-i18next`, use `t()` and write the English string in the relevant translations file in `web/public/locales/en`. +- Always conform new and refactored code to the existing coding style in the project. +- Always have a way to test your work and confirm your changes. When running backend tests, use `python3 -u -m unittest`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44f472beb..54df536d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - PYTHON_VERSION: 3.9 + PYTHON_VERSION: 3.11 jobs: amd64_build: @@ -23,7 +23,7 @@ jobs: name: AMD64 Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -47,7 +47,7 @@ jobs: name: ARM Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -77,42 +77,12 @@ jobs: 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_jp5_build: - if: false - runs-on: ubuntu-22.04 - name: Jetson Jetpack 5 - steps: - - name: Check out code - uses: actions/checkout@v4 - 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 5) - env: - ARCH: arm64 - BASE_IMAGE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - SLIM_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - TRT_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - uses: docker/bake-action@v6 - with: - source: . - push: true - targets: tensorrt - files: docker/tensorrt/trt.hcl - set: | - tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp5 - *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp5 - *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp5,mode=max jetson_jp6_build: runs-on: ubuntu-22.04-arm name: Jetson Jetpack 6 steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -143,7 +113,7 @@ jobs: - amd64_build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -166,7 +136,6 @@ jobs: *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max - name: AMD/ROCm general build env: - AMDGPU: gfx HSA_OVERRIDE: 0 uses: docker/bake-action@v6 with: @@ -185,7 +154,7 @@ jobs: - arm64_build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -203,6 +172,31 @@ jobs: 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@v6 + 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: @@ -217,7 +211,7 @@ jobs: with: string: ${{ github.repository }} - name: Log in to the Container registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 02fde5861..c4d8aa7a0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,48 +4,24 @@ on: pull_request: paths-ignore: - "docs/**" - - ".github/**" + - ".github/*.yml" + - ".github/DISCUSSION_TEMPLATE/**" + - ".github/ISSUE_TEMPLATE/**" env: DEFAULT_PYTHON: 3.11 jobs: - build_devcontainer: - runs-on: ubuntu-latest - name: Build Devcontainer - # The Dockerfile contains features that requires buildkit, and since the - # devcontainer cli uses docker-compose to build the image, the only way to - # ensure docker-compose uses buildkit is to explicitly enable it. - env: - DOCKER_BUILDKIT: "1" - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-node@master - with: - node-version: 20.x - - name: Install devcontainer cli - run: npm install --global @devcontainers/cli - - name: Build devcontainer - run: devcontainer build --workspace-folder . - # It would be nice to also test the following commands, but for some - # reason they don't work even though in VS Code devcontainer works. - # - name: Start devcontainer - # run: devcontainer up --workspace-folder . - # - name: Run devcontainer scripts - # run: devcontainer run-user-commands --workspace-folder . - web_lint: name: Web - Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-node@master + - uses: actions/setup-node@v6 with: - node-version: 16.x + node-version: 20.x - run: npm install working-directory: ./web - name: Lint @@ -56,10 +32,10 @@ jobs: name: Web - Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-node@master + - uses: actions/setup-node@v6 with: node-version: 20.x - run: npm install @@ -76,7 +52,7 @@ jobs: name: Python Checks steps: - name: Check out the repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python ${{ env.DEFAULT_PYTHON }} @@ -99,16 +75,21 @@ jobs: name: Python Tests steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build - run: make - - name: Run mypy - run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate - - name: Run tests - run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest + - 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: 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 index 6c19ca6cb..1fbf58f6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - id: lowercaseRepo @@ -18,7 +18,7 @@ jobs: with: string: ${{ github.repository }} - name: Log in to the Container registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 with: registry: ghcr.io username: ${{ github.actor }} @@ -39,14 +39,14 @@ jobs: 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; do + 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; do + 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/.gitignore b/.gitignore index 8456d9be0..660a378b0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ frigate/version.py web/build web/node_modules web/coverage +web/.env core !/web/**/*.ts .idea/* diff --git a/LICENSE b/LICENSE index bd7c18a7f..924cb4148 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2020 Blake Blackshear +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 @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index 04eee68d5..d1427b6df 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ default_target: local COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) -VERSION = 0.16.3 +VERSION = 0.17.0 IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) BOARDS= #Initialized empty @@ -14,12 +14,19 @@ 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) \ diff --git a/README.md b/README.md index 825a62884..1fb158b19 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@

- logo + logo

-# Frigate - NVR With Realtime Object Detection for IP Cameras +# 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 @@ -12,7 +14,7 @@ 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, Integrated GPU, or AI accelerator such as a [Hailo](https://hailo.ai/) is highly recommended. Dedicated hardware will outperform even the best CPUs with very little overhead. +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 @@ -33,6 +35,15 @@ View the documentation at https://docs.frigate.video 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 @@ -56,7 +67,7 @@ If you would like to make a donation to support development, please use [Github ### Built-in mask and zone editor
-Multi-camera scrubbing +Built-in mask and zone editor
## Translations @@ -66,3 +77,7 @@ We use [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) to support la
Translation status + +--- + +**Copyright © 2026 Frigate, Inc.** diff --git a/README_CN.md b/README_CN.md index 07fb5bd59..62df77b5c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,28 +1,31 @@

- logo + logo

-# Frigate - 一个具有实时目标检测的本地NVR - -[English](https://github.com/blakeblackshear/frigate) | \[简体中文\] +# Frigate NVR™ - 一个具有实时目标检测的本地 NVR 翻译状态 -一个完整的本地网络视频录像机(NVR),专为[Home Assistant](https://www.home-assistant.io)设计,具备AI物体检测功能。使用OpenCV和TensorFlow在本地为IP摄像头执行实时物体检测。 +[English](https://github.com/blakeblackshear/frigate) | \[简体中文\] -强烈推荐使用GPU或者AI加速器(例如[Google Coral加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/))。它们的性能甚至超过目前的顶级CPU,并且可以以极低的耗电实现更优的性能。 -- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与Home Assistant紧密集成 -- 设计上通过仅在必要时和必要地点寻找物体,最大限度地减少资源使用并最大化性能 +[![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进行通信,便于集成到其他系统中 +- 使用非常低开销的画面变动检测(也叫运动检测)来确定运行目标检测的位置 +- 使用 TensorFlow 进行目标检测,并运行在单独的进程中以达到最大 FPS +- 通过 MQTT 进行通信,便于集成到其他系统中 - 根据检测到的物体设置保留时间进行视频录制 -- 24/7全天候录制 -- 通过RTSP重新流传输以减少摄像头的连接数 -- 支持WebRTC和MSE,实现低延迟的实时观看 +- 24/7 全天候录制 +- 通过 RTSP 重新流传输以减少摄像头的连接数 +- 支持 WebRTC 和 MSE,实现低延迟的实时观看 ## 社区中文翻译文档 @@ -32,39 +35,56 @@ 如果您想通过捐赠支持开发,请使用 [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) + +欢迎加入中文讨论 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 000000000..fdbdc14d2 --- /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/benchmark.py b/benchmark.py index 1f39302a7..46adc59df 100755 --- a/benchmark.py +++ b/benchmark.py @@ -4,13 +4,13 @@ from statistics import mean import numpy as np -import frigate.util as util 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") @@ -91,7 +91,7 @@ edgetpu_process_2 = ObjectDetectProcess( ) for x in range(0, 10): - camera_process = util.Process( + camera_process = FrigateProcess( target=start, args=(x, 300, detection_queue, events[str(x)]) ) camera_process.daemon = True diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 1cf752ed5..055a1458f 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -55,7 +55,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ 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.9/go2rtc_linux_${TARGETARCH}" go2rtc +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.10/go2rtc_linux_${TARGETARCH}" go2rtc FROM wget AS tempio ARG TARGETARCH @@ -148,6 +148,7 @@ RUN --mount=type=bind,source=docker/main/install_s6_overlay.sh,target=/deps/inst 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 \ @@ -177,6 +178,8 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O 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 @@ -184,7 +187,10 @@ 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 +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 \ @@ -206,6 +212,7 @@ 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 @@ -224,9 +231,25 @@ 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 @@ -243,6 +266,10 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \ pip3 install -U /deps/wheels/*.whl +# 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 diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index 5dea3c874..330caff9f 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -19,7 +19,9 @@ apt-get -qq install --no-install-recommends -y \ nethogs \ libgl1 \ libglib2.0-0 \ - libusb-1.0.0 + libusb-1.0.0 \ + python3-h2 \ + libgomp1 # memryx detector update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 @@ -31,6 +33,18 @@ 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 @@ -78,11 +92,41 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then 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 apt-get -qq install --no-install-recommends --no-install-suggests -y \ - intel-opencl-icd=24.35.30872.31-996~22.04 intel-level-zero-gpu=1.3.29735.27-914~22.04 intel-media-va-driver-non-free=24.3.3-996~22.04 \ - libmfx1=23.2.2-880~22.04 libmfxgen1=24.2.4-914~22.04 libvpl2=1:2.13.0.0-996~22.04 + intel-media-va-driver-non-free libmfx1 libmfxgen1 libvpl2 + + apt-get -qq install -y ocl-icd-libopencl1 + + # install libtbb12 for NPU support + apt-get -qq install -y libtbb12 rm -f /usr/share/keyrings/intel-graphics.gpg rm -f /etc/apt/sources.list.d/intel-gpu-jammy.list + + # install legacy and standard intel icd and level-zero-gpu + # see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info + # needed core package + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/libigdgmm12_22.5.5_amd64.deb + dpkg -i libigdgmm12_22.5.5_amd64.deb + rm libigdgmm12_22.5.5_amd64.deb + + # legacy packages + wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb + wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-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 packages + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-opencl-icd_24.52.32224.5_amd64.deb + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-level-zero-gpu_1.6.32224.5_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-opencl-2_2.5.6+18417_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-core-2_2.5.6+18417_amd64.deb + # npu packages + wget https://github.com/oneapi-src/level-zero/releases/download/v1.21.9/level-zero_1.21.9+u22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-driver-compiler-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-fw-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-level-zero-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + + dpkg -i *.deb + rm *.deb fi if [[ "${TARGETARCH}" == "arm64" ]]; then @@ -101,6 +145,6 @@ 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.33.3/yq_linux_$(dpkg --print-architecture)" \ + "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_memryx.sh b/docker/main/install_memryx.sh new file mode 100644 index 000000000..676e06daa --- /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/requirements-dev.txt b/docker/main/requirements-dev.txt index af3ee5763..ac9d35758 100644 --- a/docker/main/requirements-dev.txt +++ b/docker/main/requirements-dev.txt @@ -1 +1,4 @@ ruff + +# types +types-peewee == 3.17.* diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index dce124897..f81fefea4 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -1,25 +1,28 @@ aiofiles == 24.1.* click == 8.1.* # FastAPI -aiohttp == 3.11.3 -starlette == 0.41.2 -starlette-context == 0.3.6 -fastapi == 0.115.* -uvicorn == 0.30.* +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.0.* +joserfc == 1.2.* cryptography == 44.0.* -pathvalidate == 3.2.* +pathvalidate == 3.3.* markupsafe == 3.0.* -python-multipart == 0.0.12 +python-multipart == 0.0.20 +# 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 == 3.1.* +onvif-zeep-async == 4.0.* paho-mqtt == 2.1.* pandas == 2.2.* peewee == 3.17.* -peewee_migrate == 1.13.* -psutil == 6.1.* +peewee_migrate == 1.14.* +psutil == 7.1.* pydantic == 2.10.* git+https://github.com/fbcotter/py3nvml#egg=py3nvml pytz == 2025.* @@ -28,7 +31,7 @@ ruamel.yaml == 0.18.* tzlocal == 5.2 requests == 2.32.* types-requests == 2.32.* -norfair == 2.2.* +norfair == 2.3.* setproctitle == 1.3.* ws4py == 0.5.* unidecode == 1.3.* @@ -37,16 +40,15 @@ titlecase == 2.4.* numpy == 1.26.* opencv-python-headless == 4.11.0.* opencv-contrib-python == 4.11.0.* -scipy == 1.14.* +scipy == 1.16.* # OpenVino & ONNX -openvino == 2024.4.* -onnxruntime-openvino == 1.20.* ; platform_machine == 'x86_64' -onnxruntime == 1.20.* ; platform_machine == 'aarch64' +openvino == 2025.3.* +onnxruntime == 1.22.* # Embeddings transformers == 4.45.* # Generative AI -google-generativeai == 0.8.* -ollama == 0.3.* +google-genai == 1.58.* +ollama == 0.6.* openai == 1.65.* # push notifications py-vapid == 1.9.* @@ -54,7 +56,7 @@ pywebpush == 2.0.* # alpr pyclipper == 1.3.* shapely == 2.0.* -Levenshtein==0.26.* +rapidfuzz==3.12.* # HailoRT Wheels appdirs==1.4.* argcomplete==2.0.* @@ -72,3 +74,12 @@ 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/rootfs/etc/s6-overlay/s6-rc.d/certsync/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run index af3bc04de..4ce1c133f 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run @@ -10,7 +10,7 @@ echo "[INFO] Starting certsync..." lefile="/etc/letsencrypt/live/frigate/fullchain.pem" -tls_enabled=`python3 /usr/local/nginx/get_tls_settings.py | jq -r .enabled` +tls_enabled=`python3 /usr/local/nginx/get_listen_settings.py | jq -r .tls.enabled` while true do 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 index 46bc3175f..7df29f8f5 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run @@ -50,6 +50,40 @@ function set_libva_version() { 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..." + echo '{}' > "${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 filter and keep only the homekit section + local cleaned_json="/tmp/cache/homekit_cleaned.json" + jq ' + # Keep only the homekit section if it exists, otherwise empty object + if has("homekit") then {homekit: .homekit} else {} end + ' "${temp_json}" > "${cleaned_json}" 2>/dev/null || { + echo '{}' > "${cleaned_json}" + } + + # Convert back to YAML and write to the config file + yq eval -P "${cleaned_json}" > "${config_path}" 2>/dev/null || { + echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config" + echo '{}' > "${config_path}" + } + + # Clean up temp files + rm -f "${temp_json}" "${cleaned_json}" +} + set_libva_version if [[ -f "/dev/shm/go2rtc.yaml" ]]; then @@ -70,6 +104,10 @@ 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 @@ -82,5 +120,7 @@ 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=/dev/shm/go2rtc.yaml +exec "${binary_path}" -config="${homekit_config_path}" -config=/dev/shm/go2rtc.yaml 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 index 273182930..8bd9b5250 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -85,7 +85,7 @@ python3 /usr/local/nginx/get_base_path.py | \ -out /usr/local/nginx/conf/base_path.conf # build templates for optional TLS support -python3 /usr/local/nginx/get_tls_settings.py | \ +python3 /usr/local/nginx/get_listen_settings.py | \ tempio -template /usr/local/nginx/templates/listen.gotmpl \ -out /usr/local/nginx/conf/listen.conf diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 1b44a8067..fb701a9b6 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -22,6 +22,31 @@ sys.path.remove("/opt/frigate") yaml = YAML() +# Check if arbitrary exec sources are allowed (defaults to False for security) +allow_arbitrary_exec = None +if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ: + allow_arbitrary_exec = 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") +): + allow_arbitrary_exec = ( + Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC")) + .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) + allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec") + +ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str( + allow_arbitrary_exec +).lower() in ("true", "1", "yes") + 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"): @@ -109,14 +134,26 @@ if LIBAVFORMAT_VERSION_MAJOR < 59: elif go2rtc_config["ffmpeg"].get("rtsp") is None: go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args -for name in go2rtc_config.get("streams", {}): + +def is_restricted_source(stream_source: str) -> bool: + """Check if a stream source is restricted (echo, expr, or exec).""" + return stream_source.strip().startswith(("echo:", "expr:", "exec:")) + + +for name in list(go2rtc_config.get("streams", {})): stream = go2rtc_config["streams"][name] if isinstance(stream, str): try: - go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format( - **FRIGATE_ENV_VARS - ) + formatted_stream = stream.format(**FRIGATE_ENV_VARS) + if not ALLOW_ARBITRARY_EXEC and is_restricted_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." @@ -124,15 +161,33 @@ for name in go2rtc_config.get("streams", {}): sys.exit(e) elif isinstance(stream, list): - for i, stream in enumerate(stream): + filtered_streams = [] + for i, stream_item in enumerate(stream): try: - go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS) + formatted_stream = stream_item.format(**FRIGATE_ENV_VARS) + if not ALLOW_ARBITRARY_EXEC and is_restricted_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] + # add birdseye restream stream if enabled if config.get("birdseye", {}).get("restream", False): birdseye: dict[str, Any] = config.get("birdseye") diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index c855fb926..46241c5ab 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -17,7 +17,9 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'request_time="$request_time" upstream_response_time="$upstream_response_time"'; + access_log /dev/stdout main; @@ -71,6 +73,8 @@ http { 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; @@ -103,6 +107,10 @@ http { 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; @@ -272,6 +280,18 @@ http { 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; @@ -300,6 +320,12 @@ http { 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"; 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 index 54c05ab3b..c945810be 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf @@ -18,6 +18,10 @@ 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; diff --git a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py b/docker/main/rootfs/usr/local/nginx/get_listen_settings.py similarity index 71% rename from docker/main/rootfs/usr/local/nginx/get_tls_settings.py rename to docker/main/rootfs/usr/local/nginx/get_listen_settings.py index d2e704056..d879db56e 100644 --- a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py +++ b/docker/main/rootfs/usr/local/nginx/get_listen_settings.py @@ -26,6 +26,10 @@ try: except FileNotFoundError: config: dict[str, Any] = {} -tls_config: dict[str, Any] = config.get("tls", {"enabled": True}) +tls_config: dict[str, any] = config.get("tls", {"enabled": True}) +networking_config = config.get("networking", {}) +ipv6_config = networking_config.get("ipv6", {"enabled": False}) -print(json.dumps(tls_config)) +output = {"tls": tls_config, "ipv6": ipv6_config} + +print(json.dumps(output)) diff --git a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl index 093d5f68e..066f872cb 100644 --- a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl +++ b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl @@ -1,33 +1,45 @@ -# intended for internal traffic, not protected by auth + +# Internal (IPv4 always; IPv6 optional) listen 5000; +{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:5000;{{ end }}{{ end }} + -{{ if not .enabled }} # intended for external traffic, protected by auth -listen 8971; +{{ if .tls }} + {{ if .tls.enabled }} + # external HTTPS (IPv4 always; IPv6 optional) + listen 8971 ssl; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971 ssl;{{ end }}{{ 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 }} + # external HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} + {{ end }} {{ else }} -# intended for external traffic, protected by auth -listen 8971 ssl; - -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; -} + # (No tls section) default to HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} {{ end }} diff --git a/docker/memryx/user_installation.sh b/docker/memryx/user_installation.sh new file mode 100644 index 000000000..b92b7e3b1 --- /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/Dockerfile b/docker/rockchip/Dockerfile index 668250439..70309f02e 100644 --- a/docker/rockchip/Dockerfile +++ b/docker/rockchip/Dockerfile @@ -11,7 +11,8 @@ 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 pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.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-* diff --git a/docker/rocm/Dockerfile b/docker/rocm/Dockerfile index 7cac69eef..9edcd6058 100644 --- a/docker/rocm/Dockerfile +++ b/docker/rocm/Dockerfile @@ -2,8 +2,7 @@ # https://askubuntu.com/questions/972516/debian-frontend-environment-variable ARG DEBIAN_FRONTEND=noninteractive -ARG ROCM=6.3.3 -ARG AMDGPU=gfx900 +ARG ROCM=1 ARG HSA_OVERRIDE_GFX_VERSION ARG HSA_OVERRIDE @@ -11,18 +10,17 @@ ARG HSA_OVERRIDE FROM wget AS rocm ARG ROCM -ARG AMDGPU -RUN apt update && \ +RUN apt update -qq && \ apt install -y wget gpg && \ - wget -O rocm.deb https://repo.radeon.com/amdgpu-install/$ROCM/ubuntu/jammy/amdgpu-install_6.3.60303-1_all.deb && \ + wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.1.1/ubuntu/jammy/amdgpu-install_7.1.1.70101-1_all.deb && \ apt install -y ./rocm.deb && \ apt update && \ - apt install -y rocm + 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* /opt/rocm-dist/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 @@ -33,7 +31,13 @@ RUN echo /opt/rocm/lib|tee /opt/rocm-dist/etc/ld.so.conf.d/rocm.conf ####################################################################### FROM deps AS deps-prelim -RUN apt-get update && apt-get install -y libnuma1 +COPY docker/rocm/debian-backports.sources /etc/apt/sources.list.d/debian-backports.sources +RUN apt-get update && \ + apt-get install -y libnuma1 && \ + apt-get install -qq -y -t bookworm-backports mesa-va-drivers mesa-vulkan-drivers && \ + # Install C++ standard library headers for HIPRTC kernel compilation fallback + apt-get install -qq -y libstdc++-12-dev && \ + rm -rf /var/lib/apt/lists/* WORKDIR /opt/frigate COPY --from=rootfs / / @@ -44,26 +48,29 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ 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-openvino \ +RUN pip3 uninstall -y onnxruntime \ && pip3 install -r /requirements.txt ####################################################################### FROM scratch AS rocm-dist ARG ROCM -ARG AMDGPU COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/ -COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*$AMDGPU* /opt/rocm-$ROCM/share/miopen/db/ -COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx908* /opt/rocm-$ROCM/share/miopen/db/ -COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*$AMDGPU* /opt/rocm-$ROCM/lib/rocblas/library/ +# Copy MIOpen database files for gfx10xx and gfx11xx only (RDNA2/RDNA3) +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 rocBLAS library files for gfx10xx and gfx11xx 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-dist/ / ####################################################################### FROM deps-prelim AS rocm-prelim-hsa-override0 -ENV HSA_ENABLE_SDMA=0 -ENV MIGRAPHX_ENABLE_NHWC=1 -ENV TF_ROCM_USE_IMMEDIATE_MODE=1 +ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1 +ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1 +ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1 +ENV MIGRAPHX_ENABLE_HIPRTC_WORKAROUNDS=1 COPY --from=rocm-dist / / diff --git a/docker/rocm/debian-backports.sources b/docker/rocm/debian-backports.sources new file mode 100644 index 000000000..fc51f4eeb --- /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 index 85450768e..b6a202f93 100644 --- a/docker/rocm/requirements-wheels-rocm.txt +++ b/docker/rocm/requirements-wheels-rocm.txt @@ -1 +1 @@ -onnxruntime-rocm @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v6.3.3/onnxruntime_rocm-1.20.1-cp311-cp311-linux_x86_64.whl \ No newline at end of file +onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.1.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl \ No newline at end of file diff --git a/docker/rocm/rocm.hcl b/docker/rocm/rocm.hcl index 6a84b350d..6595066c5 100644 --- a/docker/rocm/rocm.hcl +++ b/docker/rocm/rocm.hcl @@ -1,8 +1,5 @@ -variable "AMDGPU" { - default = "gfx900" -} variable "ROCM" { - default = "6.3.3" + default = "7.1.1" } variable "HSA_OVERRIDE_GFX_VERSION" { default = "" @@ -38,7 +35,6 @@ target rocm { } platforms = ["linux/amd64"] args = { - AMDGPU = AMDGPU, 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 index c92a458f5..f98d38772 100644 --- a/docker/rocm/rocm.mk +++ b/docker/rocm/rocm.mk @@ -1,53 +1,15 @@ BOARDS += rocm -# AMD/ROCm is chunky so we build couple of smaller images for specific chipsets -ROCM_CHIPSETS:=gfx900:9.0.0 gfx1030:10.3.0 gfx1100:11.0.0 - local-rocm: version - $(foreach chipset,$(ROCM_CHIPSETS), \ - AMDGPU=$(word 1,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE_GFX_VERSION=$(word 2,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE=1 \ - docker buildx bake --file=docker/rocm/rocm.hcl rocm \ - --set rocm.tags=frigate:latest-rocm-$(word 1,$(subst :, ,$(chipset))) \ - --load \ - &&) true - - unset HSA_OVERRIDE_GFX_VERSION && \ - HSA_OVERRIDE=0 \ - AMDGPU=gfx \ docker buildx bake --file=docker/rocm/rocm.hcl rocm \ --set rocm.tags=frigate:latest-rocm \ --load build-rocm: version - $(foreach chipset,$(ROCM_CHIPSETS), \ - AMDGPU=$(word 1,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE_GFX_VERSION=$(word 2,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE=1 \ - docker buildx bake --file=docker/rocm/rocm.hcl rocm \ - --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm-$(chipset) \ - &&) true - - unset HSA_OVERRIDE_GFX_VERSION && \ - HSA_OVERRIDE=0 \ - AMDGPU=gfx \ docker buildx bake --file=docker/rocm/rocm.hcl rocm \ --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm push-rocm: build-rocm - $(foreach chipset,$(ROCM_CHIPSETS), \ - AMDGPU=$(word 1,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE_GFX_VERSION=$(word 2,$(subst :, ,$(chipset))) \ - HSA_OVERRIDE=1 \ - docker buildx bake --file=docker/rocm/rocm.hcl rocm \ - --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm-$(chipset) \ - --push \ - &&) true - - unset HSA_OVERRIDE_GFX_VERSION && \ - HSA_OVERRIDE=0 \ - AMDGPU=gfx \ 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/synaptics/Dockerfile b/docker/synaptics/Dockerfile new file mode 100644 index 000000000..6a60fe43b --- /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 000000000..a22fb446a --- /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 000000000..64cb8586b --- /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 index 906e113a8..cdf5df9ff 100644 --- a/docker/tensorrt/Dockerfile.amd64 +++ b/docker/tensorrt/Dockerfile.amd64 @@ -12,13 +12,16 @@ 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 -RUN pip3 wheel --wheel-dir=/trt-wheels -c /requirements-wheels.txt -r /requirements-tensorrt.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-openvino tensorflow-cpu \ + pip3 uninstall -y onnxruntime \ && pip3 install -U /deps/trt-wheels/*.whl COPY --from=rootfs / / diff --git a/docker/tensorrt/requirements-amd64.txt b/docker/tensorrt/requirements-amd64.txt index be4aaa066..63c68b583 100644 --- a/docker/tensorrt/requirements-amd64.txt +++ b/docker/tensorrt/requirements-amd64.txt @@ -14,5 +14,5 @@ nvidia_cusparse_cu12==12.5.1.*; platform_machine == 'x86_64' nvidia_nccl_cu12==2.23.4; platform_machine == 'x86_64' nvidia_nvjitlink_cu12==12.5.82; platform_machine == 'x86_64' onnx==1.16.*; platform_machine == 'x86_64' -onnxruntime-gpu==1.20.*; platform_machine == 'x86_64' +onnxruntime-gpu==1.22.*; 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 index c9b618180..78d659746 100644 --- a/docker/tensorrt/requirements-arm64.txt +++ b/docker/tensorrt/requirements-arm64.txt @@ -1 +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 index 3490a7897..fe89b4754 100644 --- a/docker/tensorrt/requirements-models-arm64.txt +++ b/docker/tensorrt/requirements-models-arm64.txt @@ -1,3 +1,2 @@ onnx == 1.14.0; platform_machine == 'aarch64' protobuf == 3.20.3; platform_machine == 'aarch64' -numpy == 1.23.*; platform_machine == 'aarch64' # required by python-tensorrt 8.2.1 (Jetpack 4.6) diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 818440fae..17eb2053d 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -25,7 +25,7 @@ Examples of available modules are: - `frigate.app` - `frigate.mqtt` -- `frigate.object_detection` +- `frigate.object_detection.base` - `detector.` - `watchdog.` - `ffmpeg..` NOTE: All FFmpeg logs are sent as `error` level. @@ -53,6 +53,17 @@ environment_vars: VARIABLE_NAME: variable_value ``` +#### TensorFlow Thread Configuration + +If you encounter thread creation errors during classification model training, you can limit TensorFlow's thread usage: + +```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. @@ -177,9 +188,11 @@ listen [::]:5000 ipv6only=off; 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; @@ -188,9 +201,11 @@ location /frigate { ``` ### 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: @@ -200,6 +215,7 @@ services: ``` 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 ``` @@ -218,7 +234,7 @@ To do this: ### Custom go2rtc version -Frigate currently includes go2rtc v1.9.9, there may be certain cases where you want to run a different version of go2rtc. +Frigate currently includes go2rtc v1.9.10, there may be certain cases where you want to run a different version of go2rtc. To do this: @@ -242,7 +258,7 @@ 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 r -j config.yml | curl -X POST http://frigate_host:5000/api/config/save -d @- +yq -o=json '.' config.yaml | curl -X POST 'http://frigate_host:5000/api/config/save?save_option=saveonly' --data-binary @- ``` ### Via Command Line diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index b783daa69..957667914 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -50,7 +50,7 @@ cameras: ### Configuring Minimum Volume -The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. MQTT explorer can be used on the audio topic to see what volume level is being detected. +The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. :::tip @@ -72,3 +72,106 @@ audio: - speech - yell ``` + +### 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. + +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, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features. + +```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 in your config. 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 index bf878d6bd..17718c405 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -59,6 +59,7 @@ The default session length for user authentication in Frigate is 24 hours. This 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. @@ -80,7 +81,7 @@ 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 docker secret named `FRIGATE_JWT_SECRET` in `/run/secrets/` +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 Add-on options 4. A `.jwt_secret` file in the config directory @@ -123,7 +124,7 @@ proxy: role: x-forwarded-groups ``` -Frigate supports both `admin` and `viewer` roles (see below). When using port `8971`, Frigate validates these headers and subsequent requests use the headers `remote-user` and `remote-role` for authorization. +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. @@ -133,6 +134,34 @@ 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). + +```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. + #### Port Considerations **Authenticated Port (8971)** @@ -141,6 +170,7 @@ proxy: - 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)** @@ -186,6 +216,41 @@ Frigate supports user roles to control access to certain features in the UI and - **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 + +```yaml +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 @@ -205,3 +270,42 @@ To use role-based access control, you must connect to Frigate via the **authenti 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 index c053ef369..86179a264 100644 --- a/docs/docs/configuration/autotracking.md +++ b/docs/docs/configuration/autotracking.md @@ -21,7 +21,7 @@ Frigate autotracking functions with PTZ cameras capable of relative movement wit 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. -Alternatively, you can download and run [this simple Python script](https://gist.github.com/hawkeye217/152a1d4ba80760dac95d46e143d37112), replacing the details on line 4 with your camera's IP address, ONVIF port, username, and password to check your camera. +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). diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index 75fda5b88..50d5c52aa 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -147,7 +147,7 @@ WEB Digest Algorithm - MD5 Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations. | Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | -| ---------------- | ------------------------- | -------------------------------- | ----------------------------------------------------------------------- | +| ----------------- | ------------------------- | --------------------------------- | ----------------------------------------------------------------------- | | 5MP or lower | All | http-flv | Stream is h264 | | 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 | | 6MP or higher | Older (ex: RLC-8##) | rtsp | | @@ -244,7 +244,7 @@ go2rtc: - rtspx://192.168.1.1:7441/abcdefghijk ``` -[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-rtsp) +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#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. @@ -267,6 +267,7 @@ Some community members have found better performance on Wyze cameras by using an To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's [FFmpeg Device](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg-device) support: - Preparation outside of Frigate: + - Get USB camera path. Run `v4l2-ctl --list-devices` to get a listing of locally-connected cameras available. (You may need to install `v4l-utils` in a way appropriate for your Linux distribution). In the sample configuration below, we use `video=0` to correlate with a detected device path of `/dev/video0` - Get USB camera formats & resolutions. Run `ffmpeg -f v4l2 -list_formats all -i /dev/video0` to get an idea of what formats and resolutions the USB Camera supports. In the sample configuration below, we use a width of 1024 and height of 576 in the stream and detection settings based on what was reported back. - If using Frigate in a container (e.g. Docker on TrueNAS), ensure you have USB Passthrough support enabled, along with a specific Host Device (`/dev/video0`) + Container Device (`/dev/video0`) listed. diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index 2805f1b81..47efa5bba 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -79,6 +79,12 @@ cameras: 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: ""`. @@ -89,32 +95,36 @@ An ONVIF-capable camera that supports relative movement within the field of view ## ONVIF PTZ camera recommendations -This list of working and non-working PTZ cameras is based on user feedback. +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. -| 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, among others) have been reported to not support autotracking | -| Dahua DH-SD2A500HB | ✅ | ❌ | | -| Dahua DH-SD49825GB-HNR | ✅ | ✅ | | -| Dahua DH-P5AE-PV | ❌ | ❌ | | -| Foscam R5 | ✅ | ❌ | | -| 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 incompatable. | -| 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 | +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 incompatable. | +| 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 @@ -135,3 +145,7 @@ camera_groups: icon: LuCar order: 0 ``` + +## Two-Way Audio + +See the guide [here](/configuration/live/#two-way-talk) 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 000000000..ac0b9387a --- /dev/null +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -0,0 +1,130 @@ +--- +id: object_classification +title: Object Classification +--- + +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. + +## Minimum System Requirements + +Object classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate. + +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 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. You must list which object labels should be classified. + +```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 + +- **Problem framing**: Keep classes visually distinct and relevant to the chosen object types. +- **Data collection**: Use the model’s Recent Classification tab to gather balanced examples across times of day, weather, and distances. +- **Preprocessing**: Ensure examples reflect object crops similar to Frigate’s boxes; keep the subject centered. +- **Labels**: Keep label names short and consistent; include a `none` class if you plan to ignore uncertain predictions for sub labels. +- **Threshold**: Tune `threshold` per model to reduce false assignments. Start at `0.8` and adjust based on validation. + +## 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. + +```yaml +logger: + default: info + logs: + 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 000000000..1ffdf9011 --- /dev/null +++ b/docs/docs/configuration/custom_classification/state_classification.md @@ -0,0 +1,107 @@ +--- +id: state_classification +title: State Classification +--- + +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. + +## Minimum System Requirements + +State classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate. + +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 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. You must provide at least one camera crop under `state_config.cameras`. + +```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 + +- **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. +- **Selecting training images**: Images scoring below 100% due to new conditions (e.g., first snow of the year, seasonal changes) or variations (e.g., objects temporarily in view, insects at night) are good candidates for training, as they represent scenarios different from the default state. Training these lower-scoring images that differ from existing training data helps prevent overfitting. Avoid training large quantities of images that look very similar, especially if they already score 100% as this can lead to overfitting. + +## 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. + +```yaml +logger: + default: info + logs: + 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 index d72b66639..713671a16 100644 --- a/docs/docs/configuration/face_recognition.md +++ b/docs/docs/configuration/face_recognition.md @@ -24,7 +24,7 @@ Frigate needs to first detect a `person` before it can detect and recognize a fa 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 is available. +- **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. @@ -34,7 +34,7 @@ All of these features run locally on your system. 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 is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. +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 @@ -70,9 +70,12 @@ Fine-tune face recognition with these optional parameters at the global level of - `min_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: `100`. + - 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). + - Default: `None`. + - Note: This setting is only applicable when using the `large` model. See [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/) ## Usage @@ -111,9 +114,9 @@ When choosing images to include in the face training set it is recommended to al ::: -### Understanding the Train Tab +### Understanding the Recent Recognitions Tab -The Train 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. +The Recent Recognitions tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching. Each face image is labeled with a name (or `Unknown`) along with the confidence score of the recognition attempt. While each image can be used to train the system for a specific person, not all images are suitable for training. @@ -137,7 +140,7 @@ Once front-facing images are performing well, start choosing slightly off-angle 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 Train tab in the Frigate UI's Face Library. +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: @@ -185,7 +188,7 @@ Avoid training on images that already score highly, as this can lead to over-fit No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person. For more guidance, refer to the section above on improving recognition accuracy. -### I see scores above the threshold in the train tab, but a sub label wasn't assigned? +### I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned? The Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results. diff --git a/docs/docs/configuration/genai.md b/docs/docs/configuration/genai.md index 6e1d42c34..292bf437a 100644 --- a/docs/docs/configuration/genai.md +++ b/docs/docs/configuration/genai.md @@ -9,13 +9,12 @@ Requests for a description are sent off automatically to your AI provider at the ## Configuration -Generative AI can be enabled for all cameras or only for specific cameras. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below. +Generative AI can be enabled for all cameras or only for specific cameras. If GenAI is disabled for a camera, you can still manually generate descriptions for events using the HTTP API. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI 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_`. ```yaml genai: - enabled: True provider: gemini api_key: "{FRIGATE_GEMINI_API_KEY}" model: gemini-2.0-flash @@ -30,14 +29,17 @@ cameras: required_zones: - steps indoor_camera: - genai: - enabled: False # <- disable GenAI for your indoor camera + objects: + genai: + enabled: False # <- disable GenAI for your indoor camera ``` 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 can also be toggled dynamically for a camera via MQTT with the topic `frigate//object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_nameobjectdescriptionsset). + ## Ollama :::warning @@ -46,15 +48,27 @@ Using Ollama on CPU is not recommended, high inference times make using Generati ::: -[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It provides a nice API over [llama.cpp](https://github.com/ggerganov/llama.cpp). 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. +[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://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests). +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). + +### Model Types: Instruct vs Thinking + +Most vision-language models are available as **instruct** models, which are fine-tuned to follow instructions and respond concisely to prompts. However, some models (such as certain Qwen-VL or minigpt variants) offer both **instruct** and **thinking** versions. + +- **Instruct models** are always recommended for use with Frigate. These models generate direct, relevant, actionable descriptions that best fit Frigate's object and event summary use case. +- **Thinking models** are fine-tuned for more free-form, open-ended, and speculative outputs, which are typically not concise and may not provide the practical summaries Frigate expects. For this reason, Frigate does **not** recommend or support using thinking models. + +Some models are labeled as **hybrid** (capable of both thinking and instruct tasks). In these cases, Frigate will always use instruct-style prompts and specifically disables thinking-mode behaviors to ensure concise, useful responses. + +**Recommendation:** +Always select the `-instruct` or documented instruct/tagged variant of any model you use in your Frigate configuration. If in doubt, refer to your model provider’s documentation or model library for guidance on the correct model variant to use. ### Supported Models -You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/library). At the time of writing, this includes `llava`, `llava-llama3`, `llava-phi3`, and `moondream`. Note that Frigate will not automatically download the model you specify in your config, you must download the model to your local instance of Ollama first i.e. by running `ollama pull llava:7b` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. +You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/search?c=vision). Note that Frigate will not automatically download the model you specify in your config, you must download the model to your local instance of Ollama first i.e. by running `ollama pull qwen3-vl:2b-instruct` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. :::note @@ -62,19 +76,22 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru ::: +#### Ollama Cloud models + +Ollama also supports [cloud models](https://ollama.com/cloud), where your local Ollama instance handles requests from Frigate, but model inference is performed in the cloud. Set up Ollama locally, sign in with your Ollama account, and specify the cloud model name in your Frigate config. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud). + ### Configuration ```yaml genai: - enabled: True provider: ollama base_url: http://localhost:11434 - model: llava:7b + model: qwen3-vl:4b ``` ## Google Gemini -Google Gemini has a free tier allowing [15 queries per minute](https://ai.google.dev/pricing) to the API, which is more than sufficient for standard Frigate usage. +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 @@ -93,10 +110,9 @@ To start using Gemini, you must first get an API key from [Google AI Studio](htt ```yaml genai: - enabled: True provider: gemini api_key: "{FRIGATE_GEMINI_API_KEY}" - model: gemini-2.0-flash + model: gemini-2.5-flash ``` :::note @@ -121,7 +137,6 @@ To start using OpenAI, you must first [create an API key](https://platform.opena ```yaml genai: - enabled: True provider: openai api_key: "{FRIGATE_OPENAI_API_KEY}" model: gpt-4o @@ -149,7 +164,6 @@ To start using Azure OpenAI, you must first [create a resource](https://learn.mi ```yaml genai: - enabled: True provider: azure_openai base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview model: gpt-5-mini @@ -193,32 +207,35 @@ You are also able to define custom prompts in your configuration. ```yaml genai: - enabled: True provider: ollama base_url: http://localhost:11434 - model: llava + model: qwen3-vl:8b-instruct + +objects: 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 overriden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire. +Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire. ```yaml cameras: front_door: - genai: - 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 + 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 diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md new file mode 100644 index 000000000..3a54eeddf --- /dev/null +++ b/docs/docs/configuration/genai/config.md @@ -0,0 +1,159 @@ +--- +id: genai_config +title: Configuring Generative AI +--- + +## 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 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI 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_`. + +## Ollama + +:::warning + +Using Ollama on CPU is not recommended, high inference times make using Generative AI impractical. + +::: + +[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It provides a nice API over [llama.cpp](https://github.com/ggerganov/llama.cpp). 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://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests). + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/library). 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, it is recommended to pull the model beforehand by running `ollama pull your_model` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. + +:::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. + +::: + +:::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. + +::: + +The following models are recommended: + +| Model | Notes | +| ------------- | -------------------------------------------------------------------- | +| `qwen3-vl` | Strong visual and situational understanding, higher vram requirement | +| `Intern3.5VL` | Relatively fast with good vision comprehension | +| `gemma3` | Strong frame-to-frame understanding, slower inference times | +| `qwen2.5-vl` | Fast but capable model with good vision comprehension | + +:::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 32 GB to run the 33B models. + +::: + +### Configuration + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: minicpm-v:8b + 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 +``` + +## Google Gemini + +Google Gemini has a free tier allowing [15 queries per minute](https://ai.google.dev/pricing) to the API, which is more than sufficient for standard Frigate usage. + +### 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). At the time of writing, this includes `gemini-1.5-pro` and `gemini-1.5-flash`. + +### 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 + +```yaml +genai: + provider: gemini + api_key: "{FRIGATE_GEMINI_API_KEY}" + model: gemini-1.5-flash +``` + +## OpenAI + +OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route. + +### 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). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`. + +### 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 + +```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 +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). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`. + +### 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 and resource URL, which must include the `api-version` parameter (see the example below). The model field is not required in your configuration as the model is part of the deployment name you chose when deploying the resource. + +### Configuration + +```yaml +genai: + provider: azure_openai + base_url: https://example-endpoint.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2023-03-15-preview + 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 000000000..e3ae31393 --- /dev/null +++ b/docs/docs/configuration/genai/objects.md @@ -0,0 +1,78 @@ +--- +id: genai_objects +title: Object Descriptions +--- + +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_nameobjectdescriptionsset). + +## 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 are also able to define custom prompts in your configuration. + +```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, if you desire. + +```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 000000000..df287446c --- /dev/null +++ b/docs/docs/configuration/genai/review_summaries.md @@ -0,0 +1,134 @@ +--- +id: genai_review +title: Review Summaries +--- + +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_namereviewdescriptionsset). + +## 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. + +
+ 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, you can configure Frigate to extract frames directly from recordings at a higher resolution: + +```yaml +review: + genai: + enabled: true + 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. These concerns can be configured so that the review summaries will make note of them if the activity requires additional review. For example: + +```yaml +review: + genai: + enabled: true + additional_concerns: + - animals in the garden +``` + +### Preferred Language + +By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option: + +```yaml +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/hardware_acceleration_enrichments.md b/docs/docs/configuration/hardware_acceleration_enrichments.md index 1f894d345..fac2ffa61 100644 --- a/docs/docs/configuration/hardware_acceleration_enrichments.md +++ b/docs/docs/configuration/hardware_acceleration_enrichments.md @@ -5,24 +5,29 @@ title: Enrichments # Enrichments -Some of Frigate's enrichments can use a discrete GPU for accelerated processing. +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 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 and configure the enrichment according to its specific documentation. +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 will automatically be detected and used for enrichments in the `-rocm` Frigate image. + - 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 for enrichments and still use other dedicated hardware like a Coral or Hailo for object detection. However, one combination that is not supported is TensorRT for object detection and OpenVINO for enrichments. :::note diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md index cb8d7007b..bbbf5a640 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -3,78 +3,65 @@ id: hardware_acceleration_video title: Video Decoding --- +import CommunityBadge from '@site/src/components/CommunityBadge'; + # Video Decoding -It is highly recommended to use a 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. +It is highly recommended to use an integrated or discrete GPU for hardware acceleration video decoding in Frigate. -Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro +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. +:::info -## Raspberry Pi 3/4 +Frigate supports presets for optimal hardware accelerated video decoding: -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 Add-on, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration. +**AMD** -```yaml -# if you want to decode a h264 stream -ffmpeg: - hwaccel_args: preset-rpi-64-h264 +- [AMD](#amd-based-cpus): Frigate can utilize modern AMD integrated GPUs and AMD discrete GPUs to accelerate video decoding. -# if you want to decode a h265 (hevc) stream -ffmpeg: - hwaccel_args: preset-rpi-64-h265 -``` +**Intel** -:::note +- [Intel](#intel-based-cpus): Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video decoding. -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: +**Nvidia GPU** -```yaml -services: - frigate: - ... - devices: - - /dev/video11:/dev/video11 -``` +- [Nvidia GPU](#nvidia-gpus): Frigate can utilize most modern Nvidia GPUs to accelerate video decoding. -Or with `docker run`: +**Raspberry Pi 3/4** -```bash -docker run -d \ - --name frigate \ - ... - --device /dev/video11 \ - ghcr.io/blakeblackshear/frigate:stable -``` +- [Raspberry Pi](#raspberry-pi-34): Frigate can utilize the media engine in the Raspberry Pi 3 and 4 to slightly accelerate video decoding. -`/dev/video11` is the correct device (on Raspberry Pi 4B). You can check -by running the following and looking for `H264`: +**Nvidia Jetson** -```bash -for d in /dev/video*; do - echo -e "---\n$d" - v4l2-ctl --list-formats-ext -d $d -done -``` +- [Jetson](#nvidia-jetson): Frigate can utilize the media engine in Jetson hardware to accelerate video decoding. -Or map in all the `/dev/video*` devices. +**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. + :::info **Recommended hwaccel Preset** -| CPU Generation | Intel Driver | Recommended Preset | Notes | -| -------------- | ------------ | ------------------- | ------------------------------------ | -| gen1 - gen5 | i965 | preset-vaapi | qsv is not supported | -| 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 GPU | iHD / Xe | preset-intel-qsv-\* | | +| 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 GPU | iHD / Xe | preset-intel-qsv-\* | | ::: @@ -195,15 +182,17 @@ telemetry: If you are passing in a device path, make sure you've passed the device through to the container. -## AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver +## AMD-based CPUs -VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. +Frigate can utilize modern AMD integrated GPUs and AMD GPUs to accelerate video decoding using VAAPI. -:::note +### 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 Add-on users](advanced.md#environment_vars). -::: +### Via VAAPI + +VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. ```yaml ffmpeg: @@ -264,7 +253,7 @@ processes: :::note -`nvidia-smi` may 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` will not show `ffmpeg` processes when run inside the container [due to docker limitations](https://github.com/NVIDIA/nvidia-docker/issues/179#issuecomment-645579458). ::: @@ -300,12 +289,63 @@ If you do not see these processes, check the `docker logs` for the container and 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 Add-on, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration. + +```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 +services: + frigate: + ... + devices: + - /dev/video11:/dev/video11 +``` + +Or with `docker run`: + +```bash +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 (Orin AGX, Orin NX, Orin Nano\*, Xavier AGX, Xavier NX, TX2, TX1, Nano) +## NVIDIA Jetson -A separate set of docker images is available that is based on Jetpack/L4T. 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. +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: @@ -427,3 +467,29 @@ cameras: ``` ::: + +## 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 + +Add one of the following FFmpeg presets to your `config.yml` to enable hardware video processing: + +```yaml +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 index 36e8b7dad..5f70dd9a0 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -3,18 +3,18 @@ id: license_plate_recognition title: License Plate Recognition (LPR) --- -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 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. +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. However, LPR does not run on stationary vehicles. +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. When a plate is recognized, the details are: -- Added as a `sub_label` (if known) or the `recognized_license_plate` field (if unknown) to a tracked object. -- Viewable in the Review Item Details pane in Review (sub labels). +- 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) 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) and `plate`. +- 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 @@ -31,6 +31,7 @@ In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle` ## 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 is required. + ## Configuration License plate recognition is disabled by default. Enable it in your config file: @@ -66,12 +67,15 @@ Fine-tune the LPR feature using these optional parameters at the global level of - **`min_area`**: Defines the minimum area (in pixels) a license plate must be before recognition runs. - Default: `1000` pixels. Note: this is intentionally set very low as it 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. -- **`device`**: Device to use to run license plate recognition models. - - Default: `CPU` - - This can be `CPU` or `GPU`. For users without a model that detects license plates natively, using a GPU may increase performance of the models, especially the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. -- **`model_size`**: The size of the model used to detect text on plates. +- **`device`**: Device to use to run license plate detection _and_ recognition models. + - Default: `None` + - This is 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. However, for users who run a model that detects `license_plate` natively, there is little to no performance gain reported with running LPR on GPU compared to the CPU. +- **`model_size`**: The size of the model used to identify regions of text on plates. - Default: `small` - - This can be `small` or `large`. The `large` model uses an enhanced text detector and is more accurate at finding text on plates but slower than the `small` model. For most users, the small model is recommended. For users in countries with multiple lines of text on plates, the large model is recommended. Note that using the large model does not improve _text recognition_, but it may improve _text detection_. + - This can be `small` or `large`. + - 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. It is significantly slower than the `small` model. + - If your country or region does not use multi-line plates, you should use the `small` model as performance is much better for single-line plates. ### Recognition @@ -101,6 +105,32 @@ Fine-tune the LPR feature using these optional parameters at the global level of - This setting is best adjusted at the camera level if running LPR on multiple cameras. - If Frigate is already recognizing plates correctly, leave this setting 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 5 and adjusting as needed. You should see how different enhancement levels affect your plates. Use the `debug_save_plates` configuration option (see below). +### Normalization Rules + +- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially and are applied _before_ the `format` regex, if specified. Each rule must have a `pattern` (which can be a string or a regex) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0'). + +These rules must be defined at the global level of your `lpr` config. + +```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' +``` + +- 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 - **`debug_save_plates`**: Set to `True` 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. @@ -135,6 +165,9 @@ lpr: recognition_threshold: 0.85 format: "^[A-Z]{2} [A-Z][0-9]{4}$" # Only recognize plates that are two letters, followed by a space, followed by a single letter and 4 numbers match_distance: 1 # Allow one character variation in plate matching + replace_rules: + - pattern: "O" + replacement: "0" # Replace the letter O with the number 0 in every plate known_plates: Delivery Van: - "RJ K5678" @@ -145,7 +178,7 @@ lpr: :::note -If you want to detect cars on cameras but don't want to use resources to run LPR on those cars, you should disable LPR for those specific cameras. +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: ```yaml cameras: @@ -273,7 +306,7 @@ With this setup: - 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 plate, publishing will occur with an updated `sub_label` field. If characters are recognized, publishing will occur with an updated `recognized_license_plate` field. +- 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. @@ -341,9 +374,19 @@ Use `match_distance` to allow small character mismatches. Alternatively, define 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. Enable debug logs to see exactly what Frigate is doing. +1. Start with a simplified LPR config. - - 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. + - 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. + + ```yaml + lpr: + enabled: true + 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: @@ -352,7 +395,7 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is frigate.data_processing.common.license_plate: debug ``` -2. Ensure your plates are being _detected_. +3. Ensure your plates are being _detected_. If you are using a Frigate+ or `license_plate` detecting model: @@ -365,7 +408,7 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is - 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. -3. Ensure the characters on detected plates are being _recognized_. +4. Ensure the characters on detected plates are being _recognized_. - 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. @@ -389,6 +432,6 @@ If you are using a model that natively detects `license_plate`, add an _object m If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required. -### I see "Error running ... model" in my logs. How can I fix this? +### I see "Error running ... model" in my logs, or my inference time is very high. How can I fix this? This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs. diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 93c795d2f..910cb69f1 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -177,6 +177,10 @@ For devices that support two way talk, Frigate can be configured to use the feat 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. @@ -213,6 +217,42 @@ For restreamed cameras, go2rtc remains active but does not use system resources Note that disabling a camera through the config file (`enabled: False`) removes all related UI elements, including historical footage access. To retain access while disabling the camera, keep it enabled in the config and use the UI or MQTT to disable it temporarily. +### 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?** @@ -229,7 +269,27 @@ Note that disabling a camera through the config file (`enabled: False`) removes 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. - If you are still experiencing Frigate falling back to low bandwidth mode, you may need to adjust your camera's settings per the [recommendations above](#camera_settings_recommendations). + 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?** @@ -256,3 +316,38 @@ Note that disabling a camera through the config file (`enabled: False`) removes 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 + ``` diff --git a/docs/docs/configuration/masks.md b/docs/docs/configuration/masks.md index 4b57be964..4a4722586 100644 --- a/docs/docs/configuration/masks.md +++ b/docs/docs/configuration/masks.md @@ -28,7 +28,6 @@ To create a poly mask: 5. Click the plus icon under the type of mask or zone you would like to create 6. Click on the camera's latest image to create the points for a masked area. Click the first point again to close the polygon. 7. When you've finished creating your mask, press Save. -8. Restart Frigate to apply your changes. Your config file will be updated with the relative coordinates of the mask/zone: diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index 7016bf4b6..d4a7f5566 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -3,6 +3,8 @@ id: object_detectors title: Object Detectors --- +import CommunityBadge from '@site/src/components/CommunityBadge'; + # Supported Hardware :::info @@ -11,14 +13,20 @@ Frigate supports multiple different detectors that work on different types of ha **Most Hardware** -- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices. +- [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. @@ -28,15 +36,19 @@ Frigate supports multiple different detectors that work on different types of ha - [ONNX](#onnx): TensorRT will automatically be detected and used as a detector in the `-tensorrt` Frigate image when a supported ONNX model is configured. -**Nvidia Jetson** +**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** +**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. + **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. @@ -53,16 +65,14 @@ This does not affect using hardware for accelerating other tasks such as [semant # Officially Supported Detectors -Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `hailo8l`, `onnx`, `openvino`, `rknn`, and `tensorrt`. 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. +Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `hailo8l`, `memryx`, `onnx`, `openvino`, `rknn`, and `tensorrt`. 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 a TensorFlow Lite model utilizing the Google Coral delegate for hardware acceleration. To configure an Edge TPU detector, set the `"type"` attribute to `"edgetpu"`. +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. -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`. - :::tip See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edge TPU is not detected. @@ -134,6 +144,44 @@ detectors: device: pci ``` +### EdgeTPU Supported Models + +| Model | Notes | +| ----------------------- | ------------------------------------------- | +| [Mobiledet](#mobiledet) | Default model | +| [YOLOv9](#yolov9) | More accurate but slower than default model | + +#### Mobiledet + +A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + +#### YOLOv9 + +YOLOv9 models that are compiled for TensorFlow Lite and properly quantized are supported, but not included by default. [Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes. + +
+ YOLOv9 Setup & Config + +After placing the downloaded files for the tflite model and labels in your config folder, you can use the following configuration: + +```yaml +detectors: + coral: + type: edgetpu + device: usb + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize of the model, typically 320 + height: 320 # <--- should match the imgsize of the model, typically 320 + path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite + labelmap_path: /config/labels-coco17.txt +``` + +Note that due to hardware limitations of the Coral, the labelmap is a subset of the COCO labels and includes only 17 object classes. + +
+ --- ## Hailo-8 @@ -243,41 +291,55 @@ Hailo8 supports all models in the Hailo Model Zoo that include HailoRT post-proc ## OpenVINO Detector -The OpenVINO detector type runs an OpenVINO IR model on AMD and Intel CPUs, Intel GPUs and Intel VPU hardware. To configure an OpenVINO detector, set the `"type"` attribute to `"openvino"`. +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/2024/openvino-workflow/running-inference/inference-devices-and-modes.html). The most common devices are `CPU` and `GPU`. Currently, there is a known issue with using `AUTO`. For backwards compatibility, Frigate will attempt to use `GPU` if `AUTO` is set in your configuration. +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` device with OpenVINO. For detailed system requirements, see [OpenVINO System Requirements](https://docs.openvino.ai/2024/about-openvino/release-notes-openvino/system-requirements.html) +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 + device: GPU # or NPU ov_1: type: openvino - device: GPU + device: GPU # or NPU ``` ::: -### Supported Models +### OpenVINO Supported Models + +| Model | GPU | NPU | Notes | +| ------------------------------------- | --- | --- | ------------------------------------------------------------ | +| [YOLOv9](#yolo-v3-v4-v7-v9) | ✅ | ✅ | Recommended for GPU & NPU | +| [RF-DETR](#rf-detr) | ✅ | ✅ | Requires XE iGPU or Arc | +| [YOLO-NAS](#yolo-nas) | ✅ | ✅ | | +| [MobileNet v2](#ssdlite-mobilenet-v2) | ✅ | ✅ | Fast and lightweight model, less accurate than larger models | +| [YOLOX](#yolox) | ✅ | ? | | +| [D-FINE](#d-fine) | ❌ | ❌ | | #### SSDLite MobileNet v2 An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model. +
+ MobileNet v2 Config + Use the model configuration shown below when using the OpenVINO detector with the default OpenVINO model: ```yaml detectors: ov: type: openvino - device: GPU + device: GPU # Or NPU model: width: 300 @@ -288,6 +350,8 @@ model: labelmap_path: /openvino-model/coco_91cl_bkgr.txt ``` +
+ #### YOLOX This detector also supports YOLOX. Frigate does not come with any YOLOX models preloaded, so you will need to supply your own models. @@ -296,6 +360,9 @@ This detector also supports YOLOX. Frigate does not come with any YOLOX models p [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. +
+ YOLO-NAS Setup & Config + After placing the downloaded onnx model in your config folder, you can use the following configuration: ```yaml @@ -316,6 +383,8 @@ model: Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. +
+ #### YOLO (v3, v4, v7, v9) YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. @@ -326,9 +395,12 @@ The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv ::: +
+ YOLOv Setup & Config + :::warning -If you are using a Frigate+ YOLOv9 model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. +If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. ::: @@ -338,7 +410,7 @@ After placing the downloaded onnx model in your config folder, you can use the f detectors: ov: type: openvino - device: GPU + device: GPU # or NPU model: model_type: yolo-generic @@ -352,6 +424,8 @@ model: Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. +
+ #### RF-DETR [RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more informatoin on downloading the RF-DETR model for use in Frigate. @@ -362,6 +436,9 @@ Due to the size and complexity of the RF-DETR model, it is only recommended to b ::: +
+ RF-DETR Setup & Config + After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: ```yaml @@ -379,6 +456,8 @@ model: path: /config/model_cache/rfdetr.onnx ``` +
+ #### D-FINE [D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate. @@ -389,6 +468,9 @@ Currently D-FINE models only run on OpenVINO in CPU mode, GPUs currently fail to ::: +
+ D-FINE Setup & Config + After placing the downloaded onnx model in your config/model_cache folder, you can use the following configuration: ```yaml @@ -403,7 +485,63 @@ model: height: 640 input_tensor: nchw input_dtype: float - path: /config/model_cache/dfine_s_obj2coco.onnx + path: /config/model_cache/dfine-s.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +
+ +## 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 + +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 + +Using the detector config below will connect to the client: + +```yaml +detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 +``` + +### Apple Silicon Supported Models + +There is no default model provided, the following formats are supported: + +#### YOLO (v3, v4, v7, v9) + +YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. + +:::tip + +The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. + +::: + +When Frigate is started with the following config it will connect to the detector client and transfer the model automatically: + +```yaml +detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx labelmap_path: /labelmap/coco-80.txt ``` @@ -489,7 +627,18 @@ We unset the `HSA_OVERRIDE_GFX_VERSION` to prevent an existing override from mes $ docker exec -it frigate /bin/bash -c '(unset HSA_OVERRIDE_GFX_VERSION && /opt/rocm/bin/rocminfo |grep gfx)' ``` -### Supported Models +### ROCm Supported Models + +:::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](#supported-models) for supported models, there are some caveats: @@ -532,7 +681,15 @@ detectors: ::: -### Supported Models +### ONNX Supported Models + +| Model | Nvidia GPU | AMD GPU | Notes | +| ----------------------------- | ---------- | ------- | --------------------------------------------------- | +| [YOLOv9](#yolo-v3-v4-v7-v9-2) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | +| [RF-DETR](#rf-detr) | ✅ | ❌ | Supports CUDA Graphs for optimal Nvidia performance | +| [YOLO-NAS](#yolo-nas-1) | ⚠️ | ⚠️ | Not supported by CUDA Graphs | +| [YOLOX](#yolox-1) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | +| [D-FINE](#d-fine) | ⚠️ | ❌ | Not supported by CUDA Graphs | There is no default model provided, the following formats are supported: @@ -540,6 +697,9 @@ There is no default model provided, the following formats are supported: [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. +
+ YOLO-NAS Setup & Config + :::warning If you are using a Frigate+ YOLO-NAS model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. @@ -563,6 +723,8 @@ model: labelmap_path: /labelmap/coco-80.txt ``` +
+ #### YOLO (v3, v4, v7, v9) YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. @@ -573,9 +735,12 @@ The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv ::: +
+ YOLOv Setup & Config + :::warning -If you are using a Frigate+ YOLOv9 model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. +If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. ::: @@ -596,12 +761,17 @@ model: labelmap_path: /labelmap/coco-80.txt ``` +
+ Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. #### YOLOx [YOLOx](https://github.com/Megvii-BaseDetection/YOLOX) models are supported, but not included by default. See [the models section](#downloading-yolo-models) for more information on downloading the YOLOx model for use in Frigate. +
+ YOLOx Setup & Config + After placing the downloaded onnx model in your config folder, you can use the following configuration: ```yaml @@ -621,10 +791,15 @@ model: Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. +
+ #### RF-DETR [RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more information on downloading the RF-DETR model for use in Frigate. +
+ RF-DETR Setup & Config + After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: ```yaml @@ -641,10 +816,15 @@ model: path: /config/model_cache/rfdetr.onnx ``` +
+ #### D-FINE [D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate. +
+ D-FINE Setup & Config + After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: ```yaml @@ -662,6 +842,8 @@ model: labelmap_path: /labelmap/coco-80.txt ``` +
+ Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. ## CPU Detector (not recommended) @@ -717,6 +899,196 @@ To verify that the integration is working correctly, start Frigate and observe t # 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 + +To configure the MemryX detector, use the following example configuration: + +#### Single PCIe MemryX MX3 + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 +``` + +#### Multiple PCIe MemryX MX3 Modules + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + + memx1: + type: memryx + device: PCIe:1 + + memx2: + type: memryx + device: PCIe:2 +``` + +### Supported Models + +MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + +#### YOLO-NAS + +The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded from the [Models Section](#downloading-yolo-nas-model) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +**Note:** The default model for the MemryX detector is YOLO-NAS 320x320. + +The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. + +- The default size of **320x320** is optimized for lower CPU usage and faster inference times. + +##### Configuration + +Below is the recommended configuration for using the **YOLO-NAS** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolonas + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolonas.zip + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### YOLOv9 + +The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) like in the [Models Section](#yolov9-1) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +##### Configuration + +Below is the recommended configuration for using the **YOLOv9** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolo-generic + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolov9.zip + # The .zip file must contain: + # ├── yolov9.dfp (a file ending with .dfp) +``` + +#### YOLOX + +The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP. + +##### Configuration + +Below is the recommended configuration for using the **YOLOX** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolox + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float_denorm + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolox.zip + # The .zip file must contain: + # ├── yolox.dfp (a file ending with .dfp) +``` + +#### SSDLite MobileNet v2 + +The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP. + +##### Configuration + +Below is the recommended configuration for using the **SSDLite MobileNet v2** model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: ssd + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/ssdlite_mobilenet.zip + # The .zip file must contain: + # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) + # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### Using a Custom Model + +To use your own model: + +1. Package your compiled model into a `.zip` file. + +2. The `.zip` 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 the `labelmap_path` to match your custom model's labels. + +For detailed instructions on compiling models, refer to the [MemryX Compiler](https://developer.memryx.com/tools/neural_compiler.html#usage) docs and [Tutorials](https://developer.memryx.com/tutorials/tutorials.html). + +```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. @@ -799,6 +1171,41 @@ model: height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416 ``` +## 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 + +When configuring the Synap detector, you have to specify the model: a local **path**. + +#### SSD Mobilenet + +A synap model is provided in the container at /mobilenet.synap and is used by this detector type by default. The model comes from [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80). + +Use the model configuration shown below when using the synaptics detector with the default synap model: + +```yaml +detectors: # required + synap_npu: # required + type: synaptics # required + +model: # required + path: /synaptics/mobilenet.synap # required + width: 224 # required + height: 224 # required + tensor_format: nhwc # default value (optional. If you change the model, it is required) + labelmap_path: /labelmap/coco-80.txt # required +``` + ## Rockchip platform Hardware accelerated object detection is supported on the following SoCs: @@ -842,7 +1249,7 @@ $ cat /sys/kernel/debug/rknpu/load ::: -### Supported Models +### 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. @@ -968,6 +1375,105 @@ Explanation of the paramters: - **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 + +#### 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, changing the `config.yml` file is simple. + +```yaml +degirum_detector: + type: degirum + location: degirum # Set to service name (degirum_detector), container_name (degirum), or a host:port (192.168.29.4:8778) + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. If you aren't pulling a model from the AI Hub, leave this and 'token' blank. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server +``` + +Setting up a model in the `config.yml` is similar to setting up an AI server. +You can set it to: + +- 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 `config.yml` file. + +```yaml +degirum_detector: + type: degirum + location: "@local" # For accessing AI Hub devices and models + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server +``` + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```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 `config.yml` file. + +```yaml +degirum_detector: + type: degirum + location: "@cloud" # For accessing AI Hub devices and models + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the (AI Hub)[https://hub.degirum.com). +``` + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```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 +``` + # Models Some model types are not included in Frigate by default. diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md index 52c0f0c88..4dfd8b77c 100644 --- a/docs/docs/configuration/record.md +++ b/docs/docs/configuration/record.md @@ -13,34 +13,34 @@ H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other br ### 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 config will store all video for 3 days. After 3 days, only video containing motion and overlapping with alerts or detections will be retained until 30 days have passed. +For users deploying Frigate in environments where it is important to have contiguous video stored even if there was no detectable motion, the following config 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. ```yaml record: enabled: True - retain: + continuous: days: 3 - mode: all + motion: + days: 7 alerts: retain: days: 30 - mode: motion + mode: all detections: retain: days: 30 - mode: motion + mode: all ``` ### Reduced storage: Only saving video when motion is detected -In order to reduce storage requirements, you can adjust your config to only retain video where motion was detected. +In order to reduce storage requirements, you can adjust your config to only retain video where motion / activity was detected. ```yaml record: enabled: True - retain: + motion: days: 3 - mode: motion alerts: retain: days: 30 @@ -53,12 +53,12 @@ record: ### Minimum: Alerts only -If you only want to retain video that occurs during a tracked object, this config will discard video unless an alert is ongoing. +If you only want to retain video that occurs during activity caused by tracked object(s), this config will discard video unless an alert is ongoing. ```yaml record: enabled: True - retain: + continuous: days: 0 alerts: retain: @@ -80,15 +80,17 @@ Retention configs support decimals meaning they can be configured to retain `0.5 ::: -### Continuous Recording +### Continuous and Motion Recording -The number of days to retain continuous recordings can be set via the following config where X is a number, by default continuous recording is disabled. +The number of days to retain continuous and motion recordings can be set via the following config where X is a number, by default continuous recording is disabled. ```yaml record: enabled: True - retain: + 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](#what-do-the-different-retain-modes-mean) @@ -112,38 +114,6 @@ This configuration will retain recording segments that overlap with alerts and d **WARNING**: Recordings still must be enabled in the config. If a camera has recordings disabled in the config, enabling via the methods listed above will have no effect. -## What do the different retain modes mean? - -Frigate saves from the stream with the `record` role in 10 second segments. These options determine which recording segments are kept for continuous recording (but can also affect tracked objects). - -Let's say you have Frigate configured so that your doorbell camera would retain the last **2** days of continuous recording. - -- With the `all` option all 48 hours of those two days would be kept and viewable. -- With the `motion` option the only parts of those 48 hours would be segments that Frigate detected motion. This is the middle ground option that won't keep all 48 hours, but will likely keep all segments of interest along with the potential for some extra segments. -- With the `active_objects` option the only segments that would be kept are those where there was a true positive object that was not considered stationary. - -The same options are available with alerts and detections, except it will only save the recordings when it overlaps with a review item of that type. - -A configuration example of the above retain modes where all `motion` segments are stored for 7 days and `active objects` are stored for 14 days would be as follows: - -```yaml -record: - enabled: True - retain: - days: 7 - mode: motion - alerts: - retain: - days: 14 - mode: active_objects - detections: - retain: - days: 14 - mode: active_objects -``` - -The above configuration example can be added globally or on a per camera basis. - ## 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. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 858b6e935..206d7012e 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -73,6 +73,12 @@ tls: # Optional: Enable TLS for port 8971 (default: shown below) enabled: True +# Optional: IPv6 configuration +networking: + # Optional: Enable IPv6 on 5000, and 8971 if tls is configured (default: shown below) + ipv6: + enabled: False + # Optional: Proxy configuration proxy: # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth @@ -82,7 +88,13 @@ proxy: # See the docs for more info. header_map: user: x-forwarded-user - role: x-forwarded-role + 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 @@ -111,7 +123,7 @@ auth: # 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: 43200 # 12 hours + 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 @@ -228,11 +240,13 @@ birdseye: 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 binry path (default: shown below) + # Optional: ffmpeg binary path (default: shown below) # can also be set to `7.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" @@ -256,6 +270,8 @@ ffmpeg: 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 @@ -275,6 +291,9 @@ detect: 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. @@ -339,6 +358,33 @@ objects: # 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: 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 @@ -351,6 +397,8 @@ review: 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 @@ -365,12 +413,36 @@ review: 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: Motion configuration # NOTE: Can be overridden at the camera level @@ -440,18 +512,18 @@ record: expire_interval: 60 # Optional: Two-way sync recordings database with disk on startup and once a day (default: shown below). sync_recordings: False - # Optional: Retention settings for recording - retain: + # 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: Mode for retention. Available options are: all, motion, and active_objects - # all - save all recording segments regardless of activity - # motion - save all recordings segments with any detected motion - # active_objects - save all recording segments with active/moving objects - # NOTE: this mode only applies when the days setting above is greater than 0 - mode: all # Optional: Recording Export Settings export: # Optional: Timelapse Output Args (default: shown below). @@ -476,7 +548,7 @@ record: # Optional: Retention settings for recordings of alerts retain: # Required: Retention days (default: shown below) - days: 14 + 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 @@ -496,7 +568,7 @@ record: # Optional: Retention settings for recordings of detections retain: # Required: Retention days (default: shown below) - days: 14 + 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 @@ -513,7 +585,7 @@ record: snapshots: # Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below) enabled: False - # Optional: save a clean PNG copy of the snapshot image (default: shown below) + # Optional: save a clean copy of the snapshot image (default: shown below) clean_copy: True # Optional: print a timestamp on the snapshots (default: shown below) timestamp: False @@ -546,6 +618,9 @@ semantic_search: # 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 @@ -564,11 +639,14 @@ face_recognition: # 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: 100 + 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 @@ -576,6 +654,7 @@ 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 @@ -598,30 +677,82 @@ lpr: 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 (default: shown below) + replace_rules: {} -# Optional: Configuration for AI generated tracked object descriptions +# Optional: Configuration for AI / LLM provider # WARNING: Depending on the provider, this will send thumbnails over the internet -# to Google or OpenAI's LLMs to generate descriptions. It can be overridden at -# the camera level (enabled: False) to enhance privacy for indoor cameras. +# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at +# the camera level to enhance privacy for indoor cameras. genai: - # Optional: Enable AI description generation (default: shown below) - enabled: False - # Required if enabled: Provider must be one of ollama, gemini, or openai + # Required: Provider must be one of ollama, gemini, or openai 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}" - # 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." + # Required: The model to use with the provider. + model: gemini-1.5-flash + # 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.9) +# Uses https://github.com/AlexxIT/go2rtc (v1.9.10) # NOTE: The default go2rtc API port (1984) must be used, # changing this port for the integrated go2rtc instance is not supported. go2rtc: @@ -720,6 +851,8 @@ cameras: # 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: "" # 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 @@ -781,7 +914,7 @@ cameras: user: admin # Optional: password for login. password: admin - # Optional: Skip TLS verification from the ONVIF server (default: shown below) + # 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. @@ -827,33 +960,27 @@ cameras: # By default the cameras are sorted alphabetically. order: 0 - # Optional: Configuration for AI generated tracked object descriptions - genai: - # Optional: Enable AI 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: 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 ui: @@ -878,10 +1005,6 @@ ui: # full: 8:15:22 PM Mountain Standard Time # (default: shown below). time_style: medium - # Optional: Ability to manually override the date / time styling to use strftime format - # https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html - # possible values are shown above (default: not set) - strftime_fmt: "%Y/%m/%d %H:%M" # Optional: Set the unit system to either "imperial" or "metric" (default: metric) # Used in the UI and in MQTT topics unit_system: metric diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 4564595cc..d6a623ccb 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -7,7 +7,7 @@ title: Restream 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.9) 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.9#configuration) for more advanced configurations and features. +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.10) 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.10#configuration) for more advanced configurations and features. :::note @@ -24,6 +24,12 @@ 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: @@ -154,9 +160,59 @@ go2rtc: 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.9#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. 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: The output will need to be passed with two curly braces `{{output}}` diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md index d9fcb5006..91f435ff0 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -39,7 +39,7 @@ If you are enabling Semantic Search for the first time, be advised that Frigate 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 Generative AI docs](/configuration/genai.md) for more information on how to automatically generate tracked object descriptions. +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` config option as `small` or `large`: @@ -78,17 +78,21 @@ Switching between V1 and V2 requires reindexing your embeddings. The embeddings ### 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. +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. ```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 and the `large` model is configured, then the GPU will be detected and used automatically. +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. @@ -102,3 +106,61 @@ See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_ 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 + +Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes a specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab. + +:::note + +Semantic Search must be enabled to use Triggers. + +::: + +### Configuration + +Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. 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 the **Settings** page and select the **Triggers** tab. +2. Choose a camera from the dropdown menu to view or manage its triggers. +3. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one. +4. 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. +5. 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/zones.md b/docs/docs/configuration/zones.md index d2a1083e6..c0a11d4f6 100644 --- a/docs/docs/configuration/zones.md +++ b/docs/docs/configuration/zones.md @@ -27,6 +27,7 @@ cameras: - entire_yard zones: entire_yard: + friendly_name: Entire yard # You can use characters from any language text coordinates: ... ``` @@ -44,8 +45,10 @@ cameras: - 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: ... ``` @@ -59,6 +62,7 @@ cameras: - entire_yard zones: entire_yard: + friendly_name: Entire yard coordinates: ... ``` @@ -82,13 +86,16 @@ cameras: 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 remain active until the object leaves. Loitering zones are only meant to be used in areas where loitering is not expected behavior. +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. ::: diff --git a/docs/docs/frigate/camera_setup.md b/docs/docs/frigate/camera_setup.md index 06d7d3b4a..64c650c13 100644 --- a/docs/docs/frigate/camera_setup.md +++ b/docs/docs/frigate/camera_setup.md @@ -11,6 +11,12 @@ Cameras configured to output H.264 video and AAC audio will offer the most compa - **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. diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index f5577a780..f7294042a 100644 --- a/docs/docs/frigate/hardware.md +++ b/docs/docs/frigate/hardware.md @@ -3,6 +3,8 @@ 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. @@ -40,7 +42,7 @@ If the EQ13 is out of stock, the link below may take you to a suggested alternat | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- | | 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+ | +| 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 @@ -53,38 +55,54 @@ Frigate supports multiple different detectors that work on different types of ha **Most Hardware** - [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices. - - [Supports many model architectures](../../configuration/object_detectors#configuration) - 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#supported-models-1) + - [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models) - Runs best on discrete AMD GPUs +**Apple Silicon** + +- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models) + - Runs well with any size models including large + - Runs via ZMQ proxy which adds some latency, only recommended for local connection + **Intel** -- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. - - [Supports majority of model architectures](../../configuration/object_detectors#supported-models) +- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection. + - [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models) - Runs best with tiny, small, or medium models **Nvidia** -- [TensortRT](#tensorrt---nvidia-gpu): TensorRT can run on Nvidia GPUs and Jetson devices. - - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#supported-models-2) +- [TensortRT](#tensorrt---nvidia-gpu): TensorRT can run on Nvidia GPUs to provide efficient object detection. + - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models) - Runs well with any size models including large -**Rockchip** +- [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#choosing-a-model) - 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. + ::: ### Hailo-8 @@ -125,14 +143,13 @@ 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 +- Intel NPUs - Most modern AMD CPUs (though this is officially not supported by Intel) - x86 & Arm64 hosts via CPU (generally not recommended) :::note -Intel NPUs have seen [limited success in community deployments](https://github.com/blakeblackshear/frigate/discussions/13248#discussioncomment-12347357), although they remain officially unsupported. - -In testing, the NPU delivered performance that was only comparable to — or in some cases worse than — the integrated GPU. +Intel B-series (Battlemage) GPUs are not officially supported with Frigate 0.17, though a user has [provided steps to rebuild the Frigate container](https://github.com/blakeblackshear/frigate/discussions/21257) with support for them. ::: @@ -145,12 +162,13 @@ Inference speeds vary greatly depending on the CPU or GPU used, some known examp | 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 | | 320: ~ 19 ms 640: ~ 54 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 | s-320: 12 ms s-640: 30 ms | 320: ~ 18 ms 640: ~ 50 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 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 | 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 | | | @@ -160,7 +178,7 @@ Frigate is able to utilize an Nvidia GPU which supports the 12.x series of CUDA #### 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. +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. @@ -175,30 +193,74 @@ There are improved capabilities in newer GPU architectures that TensorRT can ben [NVIDIA GPU Compute Capability](https://developer.nvidia.com/cuda-gpus) Inference speeds will vary greatly depending on the GPU and the model used. -`tiny` variants are faster than the equivalent non-tiny model, some known examples are below: +`tiny (t)` variants are faster than the equivalent non-tiny model, some known examples are below: -| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time | -| --------------- | ------------------------- | ------------------------- | ---------------------- | -| GTX 1070 | s-320: 16 ms | 320: 14 ms | | -| RTX 3050 | t-320: 15 ms s-320: 17 ms | 320: ~ 10 ms 640: ~ 16 ms | Nano-320: ~ 12 ms | -| RTX 3070 | t-320: 11 ms s-320: 13 ms | 320: ~ 8 ms 640: ~ 14 ms | Nano-320: ~ 9 ms | -| RTX A4000 | | 320: ~ 15 ms | | -| Tesla P40 | | 320: ~ 105 ms | | +✅ - 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 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. +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 | -| --------- | --------------------- | ------------------------- | -| AMD 780M | 320: ~ 14 ms | 320: ~ 25 ms 640: ~ 50 ms | -| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms | +| Name | YOLOv9 Inference Time | YOLO-NAS 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 | ## 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 -Frigate supports all Jetson boards, from the inexpensive Jetson Nano to the powerful Jetson Orin AGX. It will [make use of the Jetson's hardware media engine](/configuration/hardware_acceleration_video#nvidia-jetson-orin-agx-orin-nx-orin-nano-xavier-agx-xavier-nx-tx2-tx1-nano) 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). +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-orin-agx-orin-nx-orin-nano-xavier-agx-xavier-nx-tx2-tx1-nano) 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. @@ -219,6 +281,15 @@ Frigate supports hardware video processing on all Rockchip boards. However, hard 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 | + ## 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. diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index ce8a28b13..70b4b5bc1 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -56,7 +56,7 @@ services: volumes: - /path/to/your/config:/config - /path/to/your/storage:/media/frigate - - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear + - type: tmpfs # Recommended: 1GB of memory target: /tmp/cache tmpfs: size: 1000000000 @@ -229,6 +229,77 @@ If you are using `docker run`, add this option to your command `--device /dev/ha Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8l) 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/get_started/hardware_setup.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. + +#### 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: @@ -282,6 +353,37 @@ or add these options to your `docker run` command: 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). + ## Docker Running through Docker with Docker Compose is the recommended install method. @@ -299,12 +401,13 @@ services: - /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 # For intel hwaccel, needs to be updated for your hardware + - /dev/dri/renderD128:/dev/dri/renderD128 # AMD / Intel GPU, needs to be updated for your hardware + - /dev/accel:/dev/accel # Intel NPU volumes: - /etc/localtime:/etc/localtime:ro - /path/to/your/config:/config - /path/to/your/storage:/media/frigate - - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear + - type: tmpfs # Recommended: 1GB of memory target: /tmp/cache tmpfs: size: 1000000000 @@ -362,6 +465,7 @@ 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 addons do not support the nvidia runtime. ::: @@ -405,7 +509,7 @@ To install make sure you have the [community app plugin here](https://forums.unr ## 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. +[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 diff --git a/docs/docs/frigate/updating.md b/docs/docs/frigate/updating.md index a164f9296..61cb80f13 100644 --- a/docs/docs/frigate/updating.md +++ b/docs/docs/frigate/updating.md @@ -5,7 +5,7 @@ title: Updating # Updating Frigate -The current stable version of Frigate is **0.16.3**. 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.16.3). +The current stable version of Frigate is **0.17.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.17.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 Addon, etc.). Below are instructions for the most common setups. @@ -33,21 +33,21 @@ If you’re running Frigate via Docker (recommended method), follow these steps: 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.16.3` instead of `0.15.2`). For example: + - Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.17.0` instead of `0.16.3`). For example: ```yaml services: frigate: - image: ghcr.io/blakeblackshear/frigate:0.16.3 + image: ghcr.io/blakeblackshear/frigate:0.17.0 ``` - Then pull the image: ```bash - docker pull ghcr.io/blakeblackshear/frigate:0.16.3 + docker pull ghcr.io/blakeblackshear/frigate:0.17.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.16.3`, `0.16.3-tensorrt`, or `stable`): + - Pull the image with the appropriate tag (e.g., `0.17.0`, `0.17.0-tensorrt`, or `stable`): ```bash - docker pull ghcr.io/blakeblackshear/frigate:0.16.3 + docker pull ghcr.io/blakeblackshear/frigate:0.17.0 ``` 3. **Start the Container**: @@ -105,8 +105,8 @@ 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.15.2`) 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.15.2`), and re-run `docker compose up -d`. + - For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.3`) 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.3`), and re-run `docker compose up -d`. - For Home Assistant: Reinstall the previous addon version manually via the repository if needed and restart the addon. 4. Verify the old version is running again. diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md index 474dde0a2..ca50a90d3 100644 --- a/docs/docs/guides/configuring_go2rtc.md +++ b/docs/docs/guides/configuring_go2rtc.md @@ -3,17 +3,15 @@ id: configuring_go2rtc title: Configuring go2rtc --- -# Configuring go2rtc - Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect directly to your cameras. However, adding go2rtc to your configuration is required for the following 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 -# Setup a go2rtc stream +## Setup a go2rtc stream -First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. 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.9#module-streams), not just rtsp. +First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. 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.10#module-streams), not just rtsp. :::tip @@ -49,8 +47,8 @@ After adding this to the config, restart Frigate and try to watch the live strea - Check Video Codec: - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. - - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#codecs-madness) in go2rtc documentation. - - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. + - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#codecs-madness) in go2rtc documentation. + - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. ```yaml go2rtc: streams: @@ -111,11 +109,12 @@ section. ::: -## Next steps +### 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. -## Important considerations +## Homekit Configuration -If you are configuring go2rtc to publish HomeKit camera streams, on pairing the configuration is written to the `/dev/shm/go2rtc.yaml` file inside the container. These changes must be manually copied across to the `go2rtc` section of your Frigate configuration in order to persist through restarts. +To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`. diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index 3b07d8d5b..8c90a6f33 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -134,31 +134,13 @@ Now you should be able to start Frigate by running `docker compose up -d` from w 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: Add a detect stream +### Step 1: Start Frigate -First we will add the detect stream for the camera: +At this point you should be able to start Frigate and a basic config will be created automatically. -```yaml -mqtt: - enabled: False +### Step 2: Add a camera -cameras: - name_of_your_camera: # <------ Name the camera - enabled: True - ffmpeg: - inputs: - - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection - roles: - - detect -``` - -### Step 2: Start Frigate - -At this point you should be able to start Frigate and see the video feed in the UI. - -If you get an error image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with H264 RTSP cameras that support TCP connections. - -FFmpeg arguments for other types of cameras can be found [here](../configuration/camera_specific.md). +You can click the `Add Camera` button to use the camera setup wizard to get your first camera added into Frigate. ### Step 3: Configure hardware acceleration (recommended) @@ -173,7 +155,7 @@ services: frigate: ... devices: - - /dev/dri/renderD128:/dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware + - /dev/dri/renderD128:/dev/dri/renderD128 # for intel & amd hwaccel, needs to be updated for your hardware ... ``` diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md index 169a7ad31..46453b55a 100644 --- a/docs/docs/integrations/home-assistant.md +++ b/docs/docs/integrations/home-assistant.md @@ -245,6 +245,12 @@ 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 diff --git a/docs/docs/integrations/homekit.md b/docs/docs/integrations/homekit.md new file mode 100644 index 000000000..5954af41c --- /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 index 78b4b849c..535e1bb4b 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -159,11 +159,44 @@ Message published for updates to tracked object metadata, for example: } ``` +#### 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. +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. @@ -215,6 +248,20 @@ When the review activity has ended a final `end` message is published. } ``` +### `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. @@ -233,6 +280,14 @@ 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 disabled + ### `frigate//` Publishes the count of objects for the camera for use as a sensor in Home Assistant. @@ -266,6 +321,8 @@ The height and crop of snapshots can be configured in the config. 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. @@ -278,6 +335,17 @@ 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 and off. Expected values are `ON` and `OFF`. @@ -400,6 +468,22 @@ Topic to turn review detections for a camera on or off. Expected values are `ON` 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 diff --git a/docs/docs/integrations/third_party_extensions.md b/docs/docs/integrations/third_party_extensions.md index adaee2780..c26c8a13a 100644 --- a/docs/docs/integrations/third_party_extensions.md +++ b/docs/docs/integrations/third_party_extensions.md @@ -38,3 +38,7 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht ## [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 an plugin that allows to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate. diff --git a/docs/docs/plus/index.md b/docs/docs/plus/index.md index fa8f86f9c..d75c12f92 100644 --- a/docs/docs/plus/index.md +++ b/docs/docs/plus/index.md @@ -15,13 +15,11 @@ There are three model types offered in Frigate+, `mobiledet`, `yolonas`, and `yo 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 Intel, NVidia GPUs, AMD GPUs, Hailo, MemryX\*, Apple Silicon\*, and Rockchip NPUs. | - -_\* Support coming in 0.17_ +| 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 Intel, NVidia GPUs, AMD GPUs, Hailo, MemryX, Apple Silicon, and Rockchip NPUs. | ### YOLOv9 Details @@ -39,7 +37,7 @@ If you have a Hailo device, you will need to specify the hardware you have when #### Rockchip (RKNN) Support -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. Automatic conversion is coming in 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. Automatic conversion is available in 0.17 and later. ## Supported detector types @@ -55,7 +53,7 @@ Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVi | [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo-8) | `hailo8l` | `yolov9` | | [Rockchip NPU](/configuration/object_detectors#rockchip-platform)\* | `rknn` | `yolov9` | -_\* Requires manual conversion in 0.16. Automatic conversion coming in 0.17._ +_\* Requires manual conversion in 0.16. Automatic conversion available in 0.17 and later._ ## Improving your model diff --git a/docs/docs/troubleshooting/cpu.md b/docs/docs/troubleshooting/cpu.md new file mode 100644 index 000000000..a9f449ad8 --- /dev/null +++ b/docs/docs/troubleshooting/cpu.md @@ -0,0 +1,73 @@ +--- +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/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) 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. \ No newline at end of file diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md new file mode 100644 index 000000000..89495844d --- /dev/null +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -0,0 +1,60 @@ +--- +id: dummy-camera +title: Analyzing Object Detection +--- + +When investigating object detection or tracking problems, it can be helpful to replay an exported video as a temporary "dummy" camera. This lets you reproduce issues locally, iterate on configuration (detections, zones, enrichment settings), and capture logs and clips for analysis. + +## When to use + +- Replaying an exported clip to reproduce incorrect detections +- Testing configuration changes (model settings, trackers, filters) against a known clip +- Gathering deterministic logs and recordings for debugging or issue reports + +## 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` like this: + +```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 realtime and loop indefinitely, which is useful for long debugging sessions. +- `-fflags +genpts` helps generate 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 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. + +## Variables to consider in object tracking + +- The exported video will not always line up exactly with how it originally ran through Frigate (or even with the last loop). Different frames may be used on replay, which can change detections and tracking. +- Motion detection depends on the frames used; small frame shifts can change motion regions and therefore what gets passed to the detector. +- Object detection is not deterministic: models and post-processing can yield different results across runs, so you may not get identical detections or track IDs every time. + +When debugging, treat the replay as a close approximation rather than a byte-for-byte replay. Capture multiple runs, enable recording if helpful, and examine logs and saved event clips to understand variability. + +## Troubleshooting + +- No video: verify the path is correct and accessible from the Frigate process/container. +- FFmpeg errors: check the log output for ffmpeg-specific flags and adjust `input_args` accordingly for your file/container. You may also need to disable hardware acceleration (`hwaccel_args: ""`) for the dummy camera. +- No detections: confirm the camera `roles` include `detect`, and model/detector configuration is enabled. diff --git a/docs/docs/troubleshooting/edgetpu.md b/docs/docs/troubleshooting/edgetpu.md index af94a3d84..97b2b0040 100644 --- a/docs/docs/troubleshooting/edgetpu.md +++ b/docs/docs/troubleshooting/edgetpu.md @@ -1,6 +1,6 @@ --- id: edgetpu -title: Troubleshooting EdgeTPU +title: EdgeTPU Errors --- ## USB Coral Not Detected diff --git a/docs/docs/troubleshooting/gpu.md b/docs/docs/troubleshooting/gpu.md index a5b48246a..6399f92d8 100644 --- a/docs/docs/troubleshooting/gpu.md +++ b/docs/docs/troubleshooting/gpu.md @@ -1,6 +1,6 @@ --- id: gpu -title: Troubleshooting GPU +title: GPU Errors --- ## OpenVINO diff --git a/docs/docs/troubleshooting/memory.md b/docs/docs/troubleshooting/memory.md new file mode 100644 index 000000000..d062944e5 --- /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 index d26a3614e..b1f180a82 100644 --- a/docs/docs/troubleshooting/recordings.md +++ b/docs/docs/troubleshooting/recordings.md @@ -1,6 +1,6 @@ --- id: recordings -title: Troubleshooting Recordings +title: Recordings Errors --- ## I have Frigate configured for motion recording only, but it still seems to be recording even with no motion. Why? diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index be0c54c9d..dca948953 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -10,7 +10,7 @@ const config: Config = { baseUrl: "/", onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", - favicon: "img/favicon.ico", + favicon: "img/branding/favicon.ico", organizationName: "blakeblackshear", projectName: "frigate", themes: [ @@ -116,8 +116,8 @@ const config: Config = { title: "Frigate", logo: { alt: "Frigate", - src: "img/logo.svg", - srcDark: "img/logo-dark.svg", + src: "img/branding/logo.svg", + srcDark: "img/branding/logo-dark.svg", }, items: [ { diff --git a/docs/package-lock.json b/docs/package-lock.json index 3f00a21f9..be16754be 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,67 +9,139 @@ "version": "0.0.0", "dependencies": { "@docusaurus/core": "^3.7.0", - "@docusaurus/plugin-content-docs": "^3.6.3", + "@docusaurus/plugin-content-docs": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", - "@docusaurus/theme-mermaid": "^3.6.3", + "@docusaurus/theme-mermaid": "^3.7.0", "@inkeep/docusaurus": "^2.0.16", "@mdx-js/react": "^3.1.0", "clsx": "^2.1.1", - "docusaurus-plugin-openapi-docs": "^4.3.1", - "docusaurus-theme-openapi-docs": "^4.3.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.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.4.0", - "@docusaurus/types": "^3.4.0", - "@types/react": "^18.3.7" + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/types": "^3.7.0", + "@types/react": "^18.3.27" }, "engines": { "node": ">=18.0" } }, - "node_modules/@algolia/autocomplete-core": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz", - "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==", + "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/autocomplete-plugin-algolia-insights": "1.17.9", - "@algolia/autocomplete-shared": "1.17.9" + "@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.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz", - "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==", + "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.17.9" + "@algolia/autocomplete-shared": "1.19.2" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, - "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz", - "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-shared": "1.17.9" - }, - "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" - } - }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz", - "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==", + "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", @@ -77,99 +149,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.24.0.tgz", - "integrity": "sha512-pNTIB5YqVVwu6UogvdX8TqsRZENaflqMMjdY7/XIPMNGrBoNH9tewINLI7+qc9tIaOLcAp3ZldqoEwAihZZ3ig==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.24.0.tgz", - "integrity": "sha512-IF+r9RRQsIf0ylIBNFxo7c6hDxxuhIfIbffhBXEF1HD13rjhP5AVfiaea9RzbsAZoySkm318plDpH/nlGIjbRA==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.24.0.tgz", - "integrity": "sha512-p8K6tiXQTebRBxbrzWIfGCvfkT+Umml+2lzI92acZjHsvl6KYH6igOfVstKqXJRei9pvRzEEvVDNDLXDVleGTA==", + "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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.24.0.tgz", - "integrity": "sha512-jOHF0+tixR3IZJMhZPquFNdCVPzwzzXoiqVsbTvfKojeaY6ZXybgUiTSB8JNX+YpsUT8Ebhu3UvRy4mw2PbEzw==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.24.0.tgz", - "integrity": "sha512-Fx/Fp6d8UmDBHecTt0XYF8C9TAaA3qeCQortfGSZzWp4gVmtrUCFNZ1SUwb8ULREnO9DanVrM5hGE8R8C4zZTQ==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.24.0.tgz", - "integrity": "sha512-F8ypOedSMhz6W7zuT5O1SXXsdXSOVhY2U6GkRbYk/mzrhs3jWFR3uQIfeQVWmsJjUwIGZmPoAr9E+T/Zm2M4wA==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.24.0.tgz", - "integrity": "sha512-k+nuciQuq7WERNNE+hsx3DX636zIy+9R4xdtvW3PANT2a2BDGOv3fv2mta8+QUMcVTVcGe/Mo3QCb4pc1HNoxA==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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" @@ -182,121 +254,99 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.24.0.tgz", - "integrity": "sha512-/lqVxmrvwoA+OyVK4XLMdz/PJaCTW4qYchX1AZ+98fdnH3K6XM/kMydQLfP0bUNGBQbmVrF88MqhqZRnZEn/MA==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.24.0.tgz", - "integrity": "sha512-cRisDXQJhvfZCXL4hD22qca2CmW52TniOx6L7pvkaBDx0oQk1k9o+3w11fgfcCG+47OndMeNx5CMpu+K+COMzg==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.24.0.tgz", - "integrity": "sha512-JTMz0JqN2gidvKa2QCF/rMe8LNtdHaght03px2cluZaZfBRYy8TgHgkCeBspKKvV/abWJwl7J0FzWThCshqT3w==", + "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.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.24.0.tgz", - "integrity": "sha512-B2Gc+iSxct1WSza5CF6AgfNgmLvVb61d5bqmIWUZixtJIhyAC6lSQZuF+nvt+lmKhQwuY2gYjGGClil8onQvKQ==", + "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.24.0" + "@algolia/client-common": "5.46.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.24.0.tgz", - "integrity": "sha512-6E5+hliqGc5w8ZbyTAQ+C3IGLZ/GiX623Jl2bgHA974RPyFWzVSj4rKqkboUAxQmrFY7Z02ybJWVZS5OhPQocA==", + "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.24.0" + "@algolia/client-common": "5.46.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.24.0.tgz", - "integrity": "sha512-zM+nnqZpiQj20PyAh6uvgdSz+hD7Rj7UfAZwizqNP+bLvcbGXZwABERobuilkCQqyDBBH4uv0yqIcPRl8dSBEg==", + "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.24.0" + "@algolia/client-common": "5.46.0" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@antfu/install-pkg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz", - "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==", + "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": "^0.2.8", - "tinyexec": "^0.3.2" + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", - "license": "MIT", - "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", @@ -329,30 +379,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", + "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.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "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": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^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", @@ -377,15 +427,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "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.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@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": { @@ -393,24 +443,24 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "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.1" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "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.1", + "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -430,17 +480,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "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.1", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@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.27.1", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -460,13 +510,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "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.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -486,29 +536,38 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "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.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@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.14.2" + "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.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "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.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -528,14 +587,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "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.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -622,9 +681,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "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" @@ -640,39 +699,39 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "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.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@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.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "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.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "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.27.1" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -682,13 +741,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "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.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -745,13 +804,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "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.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -876,14 +935,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "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.27.1" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -925,9 +984,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "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" @@ -956,12 +1015,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "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.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -972,17 +1031,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "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.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@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.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1008,12 +1067,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "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/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1084,10 +1144,26 @@ "@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.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "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" @@ -1178,9 +1254,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "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" @@ -1240,15 +1316,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "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.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1335,14 +1411,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", - "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", + "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.1", + "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-parameters": "^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" @@ -1383,9 +1461,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "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", @@ -1399,9 +1477,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "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" @@ -1477,9 +1555,9 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", - "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "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" @@ -1542,9 +1620,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "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" @@ -1588,16 +1666,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.1.tgz", - "integrity": "sha512-TqGF3desVsTcp3WrJGj4HfKokfCXCLcHpt4PJF0D8/iT6LPd9RS82Upw3KPeyr6B22Lfd3DO8MVrmp0oRkUDdw==", + "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.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.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": { @@ -1693,13 +1771,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", - "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "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.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@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" @@ -1775,63 +1853,64 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", - "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", + "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.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@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.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.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.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.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.27.1", - "@babel/plugin-transform-classes": "^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.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-exponentiation-operator": "^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.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.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.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.27.1", - "@babel/plugin-transform-parameters": "^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.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", @@ -1844,10 +1923,10 @@ "@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.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "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": { @@ -1881,14 +1960,14 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "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.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" @@ -1901,16 +1980,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "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.27.1" + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1920,34 +1999,34 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "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.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", - "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", + "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.30.2" + "core-js-pure": "^3.43.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "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.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -1955,31 +2034,31 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "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.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@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.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "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.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2041,9 +2120,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz", - "integrity": "sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==", + "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", @@ -2059,14 +2138,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "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", @@ -2083,9 +2162,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", - "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", + "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", @@ -2101,14 +2180,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", - "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", + "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", @@ -2121,21 +2200,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.3" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "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", @@ -2151,13 +2230,13 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "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", @@ -2174,9 +2253,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", - "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "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", @@ -2192,14 +2271,43 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@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.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz", - "integrity": "sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==", + "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", @@ -2245,9 +2353,9 @@ } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -2258,9 +2366,9 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.9.tgz", - "integrity": "sha512-2UeQCGMO5+EeQsPQK2DqXp0dad+P6nIz6G2dI06APpBuYBKxZEq7CTH+UiztFQ8cB1f89dnO9+D/Kfr+JfI2hw==", + "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", @@ -2273,10 +2381,39 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2287,9 +2424,9 @@ } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.9.tgz", - "integrity": "sha512-Enj7ZIIkLD7zkGCN31SZFx4H1gKiCs2Y4taBo/v/cqaHN7p1qGrf5UTMNSjQFZ7MgClGufHx4pddwFTGL+ipug==", + "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", @@ -2302,10 +2439,39 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2316,9 +2482,9 @@ } }, "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.5.tgz", - "integrity": "sha512-9BOS535v6YmyOYk32jAHXeddRV+iyd4vRcbrEekpwxmueAXX5J8WgbceFnE4E4Pmw/ysnB9v+n/vSWoFmcLMcA==", + "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", @@ -2331,9 +2497,38 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2344,9 +2539,9 @@ } }, "node_modules/@csstools/postcss-exponential-functions": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.8.tgz", - "integrity": "sha512-vHgDXtGIBPpFQnFNDftMQg4MOuXcWnK91L/7REjBNYzQ/p2Fa/6RcnehTqCRrNtQ46PNIolbRsiDdDuxiHolwQ==", + "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", @@ -2359,9 +2554,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2397,9 +2592,9 @@ } }, "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.9.tgz", - "integrity": "sha512-quksIsFm3DGsf8Qbr9KiSGBF2w3RwxSfOfma5wbORDB1AFF15r4EVW7sUuWw3s5IAEGMqzel/dE2rQsI7Yb8mA==", + "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", @@ -2412,9 +2607,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2424,9 +2619,9 @@ } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.9.tgz", - "integrity": "sha512-duqTeUHF4ambUybAmhX9KonkicLM/WNp2JjMUbegRD4O8A/tb6fdZ7jUNdp/UUiO1FIdDkMwmNw6856bT0XF8Q==", + "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", @@ -2439,10 +2634,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2453,9 +2648,9 @@ } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.9.tgz", - "integrity": "sha512-sDpdPsoGAhYl/PMSYfu5Ez82wXb2bVkg1Cb8vsRLhpXhAk4OSlsJN+GodAql6tqc1B2G/WToxsFU6G74vkhPvA==", + "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", @@ -2468,10 +2663,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2482,9 +2677,9 @@ } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.1.tgz", - "integrity": "sha512-lECc38i1w3qU9nhrUhP6F8y4BfcQJkR1cb8N6tZNf2llM6zPkxnqt04jRCwsUgNcB3UGKDy+zLenhOYGHqCV+Q==", + "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", @@ -2497,7 +2692,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -2531,9 +2726,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz", - "integrity": "sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==", + "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", @@ -2579,9 +2774,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -2592,9 +2787,9 @@ } }, "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.8.tgz", - "integrity": "sha512-v8VU5WtrZIyEtk88WB4fkG22TGd8HyAfSFfZZQ1uNN0+arMJdZc++H3KYTfbYDpJRGy8GwADYH8ySXiILn+OyA==", + "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", @@ -2607,9 +2802,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2711,9 +2906,9 @@ } }, "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz", - "integrity": "sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==", + "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", @@ -2726,7 +2921,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2737,9 +2932,9 @@ } }, "node_modules/@csstools/postcss-media-minmax": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.8.tgz", - "integrity": "sha512-Skum5wIXw2+NyCQWUyfstN3c1mfSh39DRAo+Uh2zzXOglBG8xB9hnArhYFScuMZkzeM+THVa//mrByKAfumc7w==", + "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", @@ -2752,10 +2947,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@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" @@ -2765,9 +2960,9 @@ } }, "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz", - "integrity": "sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==", + "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", @@ -2780,9 +2975,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -2843,9 +3038,9 @@ } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.9.tgz", - "integrity": "sha512-UHrnujimwtdDw8BYDcWJtBXuJ13uc/BjAddPdfMc/RsWxhg8gG8UbvTF0tnMtHrZ4i7lwy85fPEzK1AiykMyRA==", + "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", @@ -2858,10 +3053,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2871,10 +3066,32 @@ "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.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.1.tgz", - "integrity": "sha512-Ofz81HaY8mmbP8/Qr3PZlUzjsyV5WuxWmvtYn+jhYGvvjFazTmN9R2io5W5znY1tyk2CA9uM0IPWyY4ygDytCw==", + "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", @@ -2897,9 +3114,9 @@ } }, "node_modules/@csstools/postcss-random-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.0.tgz", - "integrity": "sha512-MYZKxSr4AKfjECL8vg49BbfNNzK+t3p2OWX+Xf7rXgMaTP44oy/e8VGWu4MLnJ3NUd9tFVkisLO/sg+5wMTNsg==", + "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", @@ -2912,9 +3129,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2924,9 +3141,9 @@ } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.9.tgz", - "integrity": "sha512-+AGOcLF5PmMnTRPnOdCvY7AwvD5veIOhTWbJV6vC3hB1tt0ii/k6QOwhWfsGGg1ZPQ0JY15u+wqLR4ZTtB0luA==", + "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", @@ -2939,10 +3156,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -2978,9 +3195,9 @@ } }, "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -2991,9 +3208,9 @@ } }, "node_modules/@csstools/postcss-sign-functions": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.3.tgz", - "integrity": "sha512-4F4GRhj8xNkBtLZ+3ycIhReaDfKJByXI+cQGIps3AzCO8/CJOeoDPxpMnL5vqZrWKOceSATHEQJUO/Q/r2y7OQ==", + "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", @@ -3006,9 +3223,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -3018,9 +3235,9 @@ } }, "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.8.tgz", - "integrity": "sha512-6Y4yhL4fNhgzbZ/wUMQ4EjFUfoNNMpEXZnDw1JrlcEBHUT15gplchtFsZGk7FNi8PhLHJfCUwVKrEHzhfhKK+g==", + "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", @@ -3033,9 +3250,35 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@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" @@ -3045,9 +3288,9 @@ } }, "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz", - "integrity": "sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==", + "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", @@ -3060,7 +3303,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/color-helpers": "^5.0.2", + "@csstools/color-helpers": "^5.1.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -3071,9 +3314,9 @@ } }, "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.8.tgz", - "integrity": "sha512-YcDvYTRu7f78/91B6bX+mE1WoAO91Su7/8KSRpuWbIGUB8hmaNSRu9wziaWSLJ1lOB1aQe+bvo9BIaLKqPOo/g==", + "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", @@ -3086,9 +3329,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -3150,22 +3393,48 @@ "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": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz", - "integrity": "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==", + "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": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.9.0.tgz", - "integrity": "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-core": "1.17.9", - "@algolia/autocomplete-preset-algolia": "1.17.9", - "@docsearch/css": "3.9.0", - "algoliasearch": "^5.14.2" + "@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", @@ -3189,9 +3458,9 @@ } }, "node_modules/@docusaurus/babel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz", - "integrity": "sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ==", + "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", @@ -3204,42 +3473,41 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/bundler": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz", - "integrity": "sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg==", + "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.7.0", - "@docusaurus/cssnano-preset": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@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.2", + "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", + "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.1", + "mini-css-extract-plugin": "^2.9.2", "null-loader": "^4.0.1", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "postcss-preset-env": "^10.1.0", - "react-dev-utils": "^12.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", @@ -3247,7 +3515,7 @@ "webpackbar": "^6.0.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/faster": "*" @@ -3259,18 +3527,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", - "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", + "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.7.0", - "@docusaurus/bundler": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3278,19 +3546,19 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.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-dev-utils": "^12.0.1", "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", @@ -3299,19 +3567,19 @@ "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "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": "^4.15.2", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" }, "bin": { "docusaurus": "bin/docusaurus.mjs" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@mdx-js/react": "^3.0.0", @@ -3320,49 +3588,49 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz", - "integrity": "sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ==", + "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.4.38", + "postcss": "^8.5.4", "postcss-sort-media-queries": "^5.2.0", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/logger": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz", - "integrity": "sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA==", + "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": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz", - "integrity": "sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA==", + "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.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -3379,7 +3647,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3387,17 +3655,17 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", + "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.7.0", + "@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@*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -3406,24 +3674,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz", - "integrity": "sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g==", + "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.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3431,7 +3699,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3440,31 +3708,32 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", - "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", + "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.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3472,43 +3741,59 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz", - "integrity": "sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==", + "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.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz", - "integrity": "sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "@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": ">=18.0" + "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", @@ -3516,18 +3801,18 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz", - "integrity": "sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3535,19 +3820,19 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz", - "integrity": "sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3555,18 +3840,18 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz", - "integrity": "sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3574,23 +3859,23 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz", - "integrity": "sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA==", + "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.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3598,22 +3883,22 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz", - "integrity": "sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3621,28 +3906,29 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", - "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", + "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.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/plugin-debug": "3.7.0", - "@docusaurus/plugin-google-analytics": "3.7.0", - "@docusaurus/plugin-google-gtag": "3.7.0", - "@docusaurus/plugin-google-tag-manager": "3.7.0", - "@docusaurus/plugin-sitemap": "3.7.0", - "@docusaurus/plugin-svgr": "3.7.0", - "@docusaurus/theme-classic": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-search-algolia": "3.7.0", - "@docusaurus/types": "3.7.0" + "@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": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3650,31 +3936,30 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz", - "integrity": "sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ==", + "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.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", - "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", - "postcss": "^8.4.26", + "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", @@ -3683,7 +3968,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3691,15 +3976,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz", - "integrity": "sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A==", + "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.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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": "*", @@ -3710,7 +3995,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3719,43 +4004,49 @@ } }, "node_modules/@docusaurus/theme-mermaid": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.7.0.tgz", - "integrity": "sha512-7kNDvL7hm+tshjxSxIqYMtsLUPsEBYnkevej/ext6ru9xyLgCed+zkvTfGzTWNeq8rJIEe2YSS8/OV5gCVaPCw==", + "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.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", - "mermaid": ">=10.4", + "@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": ">=18.0" + "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.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz", - "integrity": "sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g==", + "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.8.1", - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", - "algoliasearch": "^5.17.1", - "algoliasearch-helper": "^3.22.6", + "@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", @@ -3764,7 +4055,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3772,26 +4063,27 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz", - "integrity": "sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g==", + "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": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", + "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", @@ -3820,15 +4112,16 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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", @@ -3838,40 +4131,40 @@ "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", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz", - "integrity": "sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA==", + "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.7.0", + "@docusaurus/types": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz", - "integrity": "sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA==", + "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.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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", @@ -3879,7 +4172,7 @@ "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@exodus/schemasafe": { @@ -3928,31 +4221,14 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "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.0.0", - "@antfu/utils": "^8.1.0", + "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.14.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "mlly": "^1.7.4" - } - }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "mlly": "^1.8.0" } }, "node_modules/@inkeep/docusaurus": { @@ -3961,50 +4237,6 @@ "integrity": "sha512-dQhjlvFnl3CVr0gWeJ/V/qLnDy1XYrCfkdVSa2D3gJTxI9/vOf9639Y1aPxTxO88DiXuW9CertLrZLB6SoJ2yg==", "license": "MIT" }, - "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==", - "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.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -4035,17 +4267,23 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "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/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@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" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -4057,19 +4295,10 @@ "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/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "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", @@ -4077,15 +4306,15 @@ } }, "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==", + "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.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "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", @@ -4098,6 +4327,120 @@ "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", @@ -4105,15 +4448,16 @@ "license": "MIT" }, "node_modules/@mdx-js/mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", - "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==", + "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", @@ -4141,9 +4485,9 @@ } }, "node_modules/@mdx-js/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", - "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "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" @@ -4158,9 +4502,9 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.4.0.tgz", - "integrity": "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==", + "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" @@ -4201,6 +4545,15 @@ "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", @@ -4497,16 +4850,6 @@ "url": "https://opencollective.com/parcel" } }, - "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==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "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", @@ -4555,15 +4898,15 @@ "license": "MIT" }, "node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "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.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -4577,9 +4920,9 @@ "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.34.2", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.2.tgz", - "integrity": "sha512-glfkQFJizLdq2fBkNvc2FJW0sxDb5exd0wIXhFk+WHaFLMREBC3CxRo2Zq7uJIdfV9U3YTceMbXJklpDfmmwFQ==", + "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", @@ -4671,6 +5014,12 @@ "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", @@ -4950,9 +5299,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "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": "*", @@ -5026,9 +5375,9 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "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": { @@ -5078,9 +5427,9 @@ "license": "MIT" }, "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "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": { @@ -5270,9 +5619,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "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": { @@ -5285,33 +5634,21 @@ } }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "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": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "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": "*", @@ -5348,13 +5685,15 @@ "license": "MIT" }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", - "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "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": { - "@types/react": "*", "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" } }, "node_modules/@types/html-minifier-terser": { @@ -5370,15 +5709,15 @@ "license": "MIT" }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "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.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "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": "*" @@ -5442,29 +5781,23 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "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": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "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/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/parse5": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", @@ -5478,15 +5811,15 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "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.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "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": { @@ -5496,13 +5829,13 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", - "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "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.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-redux": { @@ -5550,9 +5883,9 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "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": { @@ -5565,12 +5898,11 @@ } }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -5584,14 +5916,24 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "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": "*" + "@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": { @@ -5626,9 +5968,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "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": "*" @@ -5646,6 +5988,15 @@ "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", @@ -5827,9 +6178,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "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" @@ -5838,6 +6189,18 @@ "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", @@ -5869,9 +6232,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "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" @@ -5890,6 +6253,24 @@ "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", @@ -5950,33 +6331,34 @@ } }, "node_modules/algoliasearch": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.24.0.tgz", - "integrity": "sha512-CkaUygzZ91Xbw11s0CsHMawrK3tl+Ue57725HGRgRzKgt2Z4wvXVXRCtQfvzh8K7Tp4Zp7f1pyHAtMROtTJHxg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.0.tgz", + "integrity": "sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==", "license": "MIT", "dependencies": { - "@algolia/client-abtesting": "5.24.0", - "@algolia/client-analytics": "5.24.0", - "@algolia/client-common": "5.24.0", - "@algolia/client-insights": "5.24.0", - "@algolia/client-personalization": "5.24.0", - "@algolia/client-query-suggestions": "5.24.0", - "@algolia/client-search": "5.24.0", - "@algolia/ingestion": "1.24.0", - "@algolia/monitoring": "1.24.0", - "@algolia/recommend": "5.24.0", - "@algolia/requester-browser-xhr": "5.24.0", - "@algolia/requester-fetch": "5.24.0", - "@algolia/requester-node-http": "5.24.0" + "@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.25.0", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.25.0.tgz", - "integrity": "sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw==", + "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" @@ -5986,9 +6368,9 @@ } }, "node_modules/allof-merge": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/allof-merge/-/allof-merge-0.6.6.tgz", - "integrity": "sha512-116eZBf2he0/J4Tl7EYMz96I5Anaeio+VL0j/H2yxW9CoYQAMMv8gYcwkVRoO7XfIOv/qzSTfVzDVGAYxKFi3g==", + "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" @@ -6157,9 +6539,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "funding": [ { "type": "opencollective", @@ -6176,9 +6558,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "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" @@ -6220,13 +6602,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "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.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.4", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -6243,25 +6625,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "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.3", - "core-js-compat": "^3.40.0" + "@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.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "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.4" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6303,6 +6685,15 @@ ], "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", @@ -6331,23 +6722,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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", + "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.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "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" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -6390,21 +6781,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -6444,9 +6820,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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" @@ -6465,9 +6841,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -6484,10 +6860,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "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" @@ -6526,6 +6903,21 @@ "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", @@ -6659,9 +7051,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "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", @@ -7112,16 +7504,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "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.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -7181,9 +7573,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "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": { @@ -7196,6 +7588,12 @@ "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", @@ -7258,24 +7656,24 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "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.0", - "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", - "integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==", + "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" @@ -7352,9 +7750,9 @@ } }, "node_modules/core-js": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "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": { @@ -7363,12 +7761,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "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.24.4" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -7376,9 +7774,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", - "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "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": { @@ -7500,9 +7898,9 @@ } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -7513,9 +7911,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "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" @@ -7525,9 +7923,9 @@ } }, "node_modules/css-has-pseudo": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz", - "integrity": "sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==", + "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", @@ -7574,9 +7972,9 @@ } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -7688,9 +8086,9 @@ } }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "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", @@ -7717,9 +8115,9 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "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" @@ -7729,9 +8127,9 @@ } }, "node_modules/cssdb": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.5.tgz", - "integrity": "sha512-leAt8/hdTCtzql9ZZi86uYAmCLzVKpJMMdjbvOGVnXFXz/BWFpBmM1MHEHU/RqtPyRYmabVmEW1DtX3YGLuuLA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.5.2.tgz", + "integrity": "sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==", "funding": [ { "type": "opencollective", @@ -7887,15 +8285,15 @@ "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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.31.4", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.4.tgz", - "integrity": "sha512-JfUX/esCfnBGP+uNqRSkAr8jDr1HDSEm6jUNG+BToi43zwLisWrArZjIboB3NfCF5yKu2eG6sbPYaefEEaufyQ==", + "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" @@ -8391,9 +8789,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", - "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "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", @@ -8401,9 +8799,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "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": { @@ -8413,9 +8811,9 @@ "license": "MIT" }, "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==", + "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" @@ -8430,9 +8828,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "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" @@ -8487,16 +8885,32 @@ "node": ">=0.10.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "license": "BSD-2-Clause", + "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": { - "execa": "^5.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">= 10" + "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": { @@ -8551,28 +8965,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "license": "MIT", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -8658,38 +9050,6 @@ "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/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/detect-port-alt/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/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8737,9 +9097,9 @@ } }, "node_modules/docusaurus-plugin-openapi-docs": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.3.7.tgz", - "integrity": "sha512-wCXuHniG108OGCj6qKtTOFLgyhnlztMegj63BbEyHC/OgM7PDL2Yj2VFkWsU3eCmJKI+czahanztFMhVLFD67w==", + "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", @@ -8807,9 +9167,9 @@ } }, "node_modules/docusaurus-theme-openapi-docs": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.3.7.tgz", - "integrity": "sha512-VRKA8gFVIlSBUu7EAYOY3JDF2WetCSVsYx5WeFo8g6/7LJWHhX7/A7Wo2fJ0B61VE/c53BSdbmvVWSJoUqnkoA==", + "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", @@ -9928,9 +10288,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", - "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", + "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" @@ -10017,9 +10377,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.148", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.148.tgz", - "integrity": "sha512-8uc1QXwwqayD4mblcsQYZqoi+cOc97A2XmKSBOIRbEAvbp6vrqmSYs4dHD2qVygUgn7Mi0qdKgPaJ9WC8cv63A==", + "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": { @@ -10063,9 +10423,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "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", @@ -10088,9 +10448,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "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" @@ -10334,9 +10694,9 @@ } }, "node_modules/estree-util-value-to-estree": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.3.3.tgz", - "integrity": "sha512-Db+m1WSD4+mUO7UgMeKkAwdbfNWwIxLt48XF2oFU9emPfXkIu+k5/nlOj313v7wqtAPo0f9REhUvznFrPkG8CQ==", + "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" @@ -10425,6 +10785,15 @@ "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", @@ -10455,39 +10824,39 @@ "license": "BSD-3-Clause" }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "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", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "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", + "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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -10533,21 +10902,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10557,12 +10911,6 @@ "node": ">= 0.6" } }, - "node_modules/exsolve": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -10615,6 +10963,22 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "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", @@ -10769,15 +11133,6 @@ "node": ">=0.10.0" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -10791,17 +11146,17 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -10865,9 +11220,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "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", @@ -10890,184 +11245,6 @@ "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", "license": "MIT" }, - "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==", - "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/foreground-child/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==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -11095,15 +11272,15 @@ } }, "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==", + "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": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -11117,9 +11294,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "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", @@ -11130,12 +11307,6 @@ "node": ">=14.14" } }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "license": "Unlicense" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11277,6 +11448,22 @@ "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", @@ -11284,9 +11471,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/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==", + "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", @@ -11320,62 +11507,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-dirs/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/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -11485,9 +11616,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "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", @@ -11695,15 +11826,15 @@ } }, "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "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": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" @@ -11713,16 +11844,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-parse5/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/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -11833,22 +11954,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -11908,9 +12013,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "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", @@ -11989,9 +12094,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "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": { @@ -12001,19 +12106,23 @@ "license": "MIT" }, "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==", + "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.1", - "toidentifier": "1.0.1" + "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": { @@ -12119,6 +12228,15 @@ "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", @@ -12173,13 +12291,10 @@ } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -12198,9 +12313,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "license": "MIT" }, "node_modules/import-fresh": { @@ -12273,10 +12388,13 @@ "license": "ISC" }, "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" + "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", @@ -12312,9 +12430,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "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" @@ -12486,6 +12604,39 @@ "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", @@ -12502,10 +12653,22 @@ "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.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", + "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" @@ -12532,15 +12695,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -12583,15 +12737,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -12652,21 +12797,6 @@ "node": ">=0.10.0" } }, - "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==", - "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-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -12752,9 +12882,9 @@ "license": "MIT" }, "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==", + "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" @@ -12805,6 +12935,12 @@ "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", @@ -12847,9 +12983,9 @@ } }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "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" @@ -12859,9 +12995,9 @@ } }, "node_modules/katex": { - "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "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" @@ -12915,12 +13051,6 @@ "node": ">=6" } }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, "node_modules/langium": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", @@ -12953,13 +13083,13 @@ } }, "node_modules/launch-editor": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", - "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "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.0.0", - "shell-quote": "^1.8.1" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, "node_modules/layout-base": { @@ -13005,12 +13135,16 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "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": { @@ -13027,23 +13161,6 @@ "node": ">=8.9.0" } }, - "node_modules/local-pkg": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", - "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.0.1", - "quansync": "^0.2.8" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", @@ -13164,15 +13281,15 @@ } }, "node_modules/marked": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz", - "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "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": ">= 18" + "node": ">= 20" } }, "node_modules/math-intrinsics": { @@ -13604,9 +13721,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "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", @@ -13674,15 +13791,21 @@ } }, "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", + "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": { - "fs-monkey": "^1.0.4" + "@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" }, - "engines": { - "node": ">= 4.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/merge-descriptors": { @@ -13710,27 +13833,27 @@ } }, "node_modules/mermaid": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz", - "integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==", + "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.0.4", - "@iconify/utils": "^2.1.33", - "@mermaid-js/parser": "^0.4.0", + "@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.11", - "dayjs": "^1.11.13", - "dompurify": "^3.2.4", - "katex": "^0.16.9", + "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": "^15.0.7", + "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -15607,9 +15730,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "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", @@ -15653,42 +15776,16 @@ "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==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "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.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/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/mlly/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" + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" } }, "node_modules/mri": { @@ -15855,9 +15952,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "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" @@ -15873,9 +15970,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "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": { @@ -15897,9 +15994,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "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" @@ -16175,9 +16272,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "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" @@ -16261,6 +16358,18 @@ "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", @@ -16279,6 +16388,15 @@ "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", @@ -16324,26 +16442,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "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": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "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": ">=6" + "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": { @@ -16364,20 +16505,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/package-manager-detector": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", - "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", - "license": "MIT", - "dependencies": { - "quansync": "^0.2.7" - } + "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", @@ -16482,9 +16614,9 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "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" @@ -16573,28 +16705,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "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==", - "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-scurry/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==", - "license": "ISC" - }, "node_modules/path-to-regexp": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", @@ -16662,87 +16772,14 @@ } }, "node_modules/pkg-types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", - "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "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.2.1", - "exsolve": "^1.0.1", - "pathe": "^2.0.3" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, "node_modules/pluralize": { @@ -16771,9 +16808,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -16790,7 +16827,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16824,9 +16861,9 @@ } }, "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -16868,9 +16905,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.9.tgz", - "integrity": "sha512-WScwD3pSsIz+QP97sPkGCeJm7xUH0J18k6zV5o8O2a4cQJyv15vLUx/WFQajuJVgZhmJL5awDu8zHnqzAzm4lw==", + "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", @@ -16883,10 +16920,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -16983,9 +17020,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz", - "integrity": "sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==", + "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", @@ -16998,10 +17035,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@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" @@ -17011,9 +17048,9 @@ } }, "node_modules/postcss-custom-properties": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz", - "integrity": "sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==", + "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", @@ -17026,9 +17063,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@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" }, @@ -17040,9 +17077,9 @@ } }, "node_modules/postcss-custom-selectors": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz", - "integrity": "sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==", + "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", @@ -17055,9 +17092,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@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": { @@ -17068,9 +17105,9 @@ } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17106,9 +17143,9 @@ } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17182,9 +17219,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.1.tgz", - "integrity": "sha512-ZitCwmvOR4JzXmKw6sZblTgwV1dcfLvClcyjADuqZ5hU0Uk4SVNpvSN9w8NcJ7XuxhRYxVA8m8AB3gy+HNBQOA==", + "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", @@ -17197,7 +17234,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -17234,9 +17271,9 @@ } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17272,9 +17309,9 @@ } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17342,9 +17379,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.9.tgz", - "integrity": "sha512-IGbsIXbqMDusymJAKYX+f9oakPo89wL9Pzd/qRBQOVf3EIQWT9hgvqC4Me6Dkzxp3KPuIBf6LPkjrLHe/6ZMIQ==", + "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", @@ -17357,10 +17394,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", + "@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": { @@ -17561,9 +17598,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17589,9 +17626,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17617,9 +17654,9 @@ } }, "node_modules/postcss-nesting": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", - "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "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", @@ -17632,7 +17669,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, @@ -17644,9 +17681,9 @@ } }, "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", - "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "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", @@ -17688,9 +17725,9 @@ } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -17931,9 +17968,9 @@ } }, "node_modules/postcss-preset-env": { - "version": "10.1.6", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.6.tgz", - "integrity": "sha512-1jRD7vttKLJ7o0mcmmYWKRLm7W14rI8K1I7Y41OeXUPEVc/CAzfTssNUeJ0zKbR+zMk4boqct/gwS/poIFF5Lg==", + "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", @@ -17946,62 +17983,68 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-cascade-layers": "^5.0.1", - "@csstools/postcss-color-function": "^4.0.9", - "@csstools/postcss-color-mix-function": "^3.0.9", - "@csstools/postcss-content-alt-text": "^2.0.5", - "@csstools/postcss-exponential-functions": "^2.0.8", + "@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.9", - "@csstools/postcss-gradients-interpolation-method": "^5.0.9", - "@csstools/postcss-hwb-function": "^4.0.9", - "@csstools/postcss-ic-unit": "^4.0.1", + "@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.1", - "@csstools/postcss-light-dark-function": "^2.0.8", + "@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.3", - "@csstools/postcss-media-minmax": "^2.0.8", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.4", + "@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.9", - "@csstools/postcss-progressive-custom-properties": "^4.0.1", - "@csstools/postcss-random-function": "^2.0.0", - "@csstools/postcss-relative-color-syntax": "^3.0.9", + "@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.3", - "@csstools/postcss-stepped-value-functions": "^4.0.8", - "@csstools/postcss-text-decoration-shorthand": "^4.0.2", - "@csstools/postcss-trigonometric-functions": "^4.0.8", + "@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.21", - "browserslist": "^4.24.4", + "autoprefixer": "^10.4.22", + "browserslist": "^4.28.0", "css-blank-pseudo": "^7.0.1", - "css-has-pseudo": "^7.0.2", + "css-has-pseudo": "^7.0.3", "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.2.5", + "cssdb": "^8.5.2", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.9", + "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.5", - "postcss-custom-properties": "^14.0.4", - "postcss-custom-selectors": "^8.0.4", + "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.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.9", + "postcss-lab-function": "^7.0.12", "postcss-logical": "^8.1.0", - "postcss-nesting": "^13.0.1", + "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^6.0.0", "postcss-page-break": "^3.0.4", @@ -18043,9 +18086,9 @@ } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -18136,9 +18179,9 @@ } }, "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "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", @@ -18385,9 +18428,9 @@ } }, "node_modules/property-information": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", - "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "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", @@ -18432,9 +18475,9 @@ } }, "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", + "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" @@ -18447,9 +18490,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "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" @@ -18461,31 +18504,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/quansync": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", - "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -18537,15 +18555,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "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.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -18656,6 +18674,12 @@ "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", @@ -18677,132 +18701,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -18816,12 +18714,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -18847,9 +18739,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.56.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz", - "integrity": "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==", + "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" @@ -18869,15 +18761,15 @@ "license": "MIT" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "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": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-lifecycles-compat": { @@ -19736,12 +19628,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -19769,9 +19655,9 @@ } }, "node_modules/recma-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz", - "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==", + "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", @@ -19783,6 +19669,9 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/recma-parse": { @@ -19817,40 +19706,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/recursive-readdir/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/recursive-readdir/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/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -19885,9 +19740,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "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" @@ -19897,17 +19752,17 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "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.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -19947,29 +19802,17 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "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.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/rehype-raw": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", @@ -20076,9 +19919,9 @@ } }, "node_modules/remark-mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz", - "integrity": "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==", + "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", @@ -20285,12 +20128,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "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.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -20359,22 +20202,6 @@ "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", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -20411,6 +20238,18 @@ "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", @@ -20479,9 +20318,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.87.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", - "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "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", @@ -20499,9 +20338,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", - "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "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" @@ -20567,10 +20406,10 @@ } }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "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", @@ -20581,10 +20420,16 @@ "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.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "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", @@ -20640,9 +20485,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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" @@ -20667,15 +20512,15 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "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": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", @@ -20705,11 +20550,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "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==", + "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" } @@ -20723,6 +20575,15 @@ "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", @@ -20748,9 +20609,9 @@ } }, "node_modules/serve-handler/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==", + "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", @@ -20889,6 +20750,88 @@ "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", @@ -20952,9 +20895,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "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" @@ -21227,12 +21170,12 @@ } }, "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "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": ">= 8" + "node": ">= 12" } }, "node_modules/source-map-js": { @@ -21322,18 +21265,18 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "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": { @@ -21362,31 +21305,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "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/string-width-cjs/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/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "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" @@ -21396,9 +21318,9 @@ } }, "node_modules/string-width/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==", + "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" @@ -21450,19 +21372,6 @@ "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==", - "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", @@ -21494,27 +21403,27 @@ } }, "node_modules/style-to-js": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", - "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "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.8" + "style-to-object": "1.0.14" } }, "node_modules/style-to-js/node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "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.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "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.4" + "inline-style-parser": "0.2.7" } }, "node_modules/style-to-object": { @@ -21549,17 +21458,17 @@ "license": "MIT" }, "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "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", - "glob": "^10.3.10", "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": { @@ -21579,41 +21488,6 @@ "node": ">= 6" } }, - "node_modules/sucrase/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==", - "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/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "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", @@ -21705,23 +21579,40 @@ "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.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "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.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "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.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -21733,9 +21624,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "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", @@ -21801,12 +21692,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "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==", - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -21828,6 +21713,34 @@ "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", @@ -21847,10 +21760,67 @@ "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==", - "license": "MIT" + "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", @@ -21888,6 +21858,22 @@ "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", @@ -21963,20 +21949,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/ufo": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", @@ -21984,9 +21956,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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": { @@ -22021,18 +21993,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "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.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "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" @@ -22083,9 +22055,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "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" @@ -22150,9 +22122,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "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", @@ -22182,9 +22154,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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", @@ -22274,9 +22246,9 @@ } }, "node_modules/update-notifier/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "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" @@ -22294,12 +22266,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uri-js-replace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", - "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", - "license": "MIT" - }, "node_modules/url": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", @@ -22404,6 +22370,15 @@ "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", @@ -22566,9 +22541,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "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", @@ -22638,9 +22613,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "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", @@ -22676,35 +22651,36 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.99.7", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", - "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", + "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.6", + "@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.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "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.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -22758,26 +22734,32 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "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": "^3.4.3", - "mime-types": "^2.1.31", + "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": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-middleware/node_modules/colorette": { @@ -22786,6 +22768,31 @@ "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", @@ -22796,54 +22803,52 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "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.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "@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.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "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": "^5.3.4", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -22860,10 +22865,40 @@ "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.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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" @@ -22896,9 +22931,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "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" @@ -23062,48 +23097,10 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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==", - "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "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" @@ -23113,9 +23110,9 @@ } }, "node_modules/wrap-ansi/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==", + "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" @@ -23125,9 +23122,9 @@ } }, "node_modules/wrap-ansi/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==", + "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" @@ -23178,6 +23175,36 @@ } } }, + "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", @@ -23301,9 +23328,9 @@ } }, "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "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" @@ -23312,6 +23339,15 @@ "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", diff --git a/docs/package.json b/docs/package.json index 4cc8a7eac..0ff76c473 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,14 +18,14 @@ }, "dependencies": { "@docusaurus/core": "^3.7.0", - "@docusaurus/plugin-content-docs": "^3.6.3", + "@docusaurus/plugin-content-docs": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", - "@docusaurus/theme-mermaid": "^3.6.3", + "@docusaurus/theme-mermaid": "^3.7.0", "@inkeep/docusaurus": "^2.0.16", "@mdx-js/react": "^3.1.0", "clsx": "^2.1.1", - "docusaurus-plugin-openapi-docs": "^4.3.1", - "docusaurus-theme-openapi-docs": "^4.3.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.1", "prism-react-renderer": "^2.4.1", "raw-loader": "^4.0.2", "react": "^18.3.1", @@ -44,9 +44,9 @@ ] }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.4.0", - "@docusaurus/types": "^3.4.0", - "@types/react": "^18.3.7" + "@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/sidebars.ts b/docs/sidebars.ts index e3de4d478..ea0d2f5c8 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -5,14 +5,14 @@ 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/glossary', + "frigate/index", + "frigate/hardware", + "frigate/planning_setup", + "frigate/installation", + "frigate/updating", + "frigate/camera_setup", + "frigate/video_pipeline", + "frigate/glossary", ], Guides: [ "guides/getting_started", @@ -28,7 +28,7 @@ const sidebars: SidebarsConfig = { { type: "link", label: "Go2RTC Configuration Reference", - href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.10#configuration", } as PropSidebarItemLink, ], Detectors: [ @@ -37,10 +37,36 @@ const sidebars: SidebarsConfig = { ], Enrichments: [ "configuration/semantic_search", - "configuration/genai", "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", + ], + }, ], Cameras: [ "configuration/cameras", @@ -90,20 +116,40 @@ const sidebars: SidebarsConfig = { items: frigateHttpApiSidebar, }, "integrations/mqtt", + "integrations/homekit", "configuration/metrics", "integrations/third_party_extensions", ], - 'Frigate+': [ - 'plus/index', - 'plus/annotating', - 'plus/first_model', - 'plus/faq', + "Frigate+": [ + "plus/index", + "plus/annotating", + "plus/first_model", + "plus/faq", ], Troubleshooting: [ "troubleshooting/faqs", "troubleshooting/recordings", - "troubleshooting/gpu", - "troubleshooting/edgetpu", + "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", diff --git a/docs/src/components/CommunityBadge/index.jsx b/docs/src/components/CommunityBadge/index.jsx new file mode 100644 index 000000000..67b9a9efa --- /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/LanguageAlert/styles.module.css b/docs/src/components/LanguageAlert/styles.module.css index f8d1e8eb2..d41584934 100644 --- a/docs/src/components/LanguageAlert/styles.module.css +++ b/docs/src/components/LanguageAlert/styles.module.css @@ -1,13 +1,18 @@ .alert { - padding: 12px; - background: #fff8e6; - border-bottom: 1px solid #ffd166; - text-align: center; - font-size: 15px; - } - - .alert a { - color: #1890ff; - font-weight: 500; - margin-left: 6px; - } \ No newline at end of file + 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/static/_headers b/docs/static/_headers new file mode 100644 index 000000000..7327dc463 --- /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 index ca53bdcf7..f1a00fe61 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -14,19 +14,38 @@ paths: get: tags: - Auth - summary: Auth + summary: Authenticate request + description: |- + Authenticates the current request based on proxy headers or JWT token. + This endpoint verifies authentication credentials and manages JWT token refresh. + On success, no JSON body is returned; authentication state is communicated via response headers and cookies. operationId: auth_auth_get responses: - "200": - description: Successful Response - content: - application/json: - schema: {} + "202": + description: Authentication Accepted (no response body, different headers depending on auth method) + 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 ("frigate-token") when applicable + schema: + type: string + "401": + description: Authentication Failed /profile: get: tags: - Auth - summary: Profile + 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. operationId: profile_profile_get responses: "200": @@ -34,11 +53,16 @@ paths: content: application/json: schema: {} + "401": + description: Unauthorized /logout: get: tags: - Auth - summary: Logout + summary: Logout user + description: |- + Logs out the current user by clearing the session cookie. + After logout, subsequent requests will require re-authentication. operationId: logout_logout_get responses: "200": @@ -46,11 +70,22 @@ paths: content: application/json: schema: {} + "303": + description: See Other (redirects to login page) /login: post: tags: - Auth - summary: Login + 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. + + Example using Bearer token: + ``` + curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile + ``` operationId: login_login_post requestBody: required: true @@ -64,6 +99,11 @@ paths: content: application/json: schema: {} + "401": + description: Login Failed - Invalid credentials + content: + application/json: + schema: {} "422": description: Validation Error content: @@ -74,7 +114,10 @@ paths: get: tags: - Auth - summary: Get Users + 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. operationId: get_users_users_get responses: "200": @@ -82,10 +125,19 @@ paths: content: application/json: schema: {} + "403": + description: Forbidden - Admin role required post: tags: - Auth - summary: Create User + summary: Create new user + description: |- + Creates a new user with the specified username, password, and role. + Requires admin role. Password must meet strength requirements: + - Minimum 8 characters + - At least one uppercase letter + - At least one digit + - At least one special character (!@#$%^&*(),.?":{}\|<>) operationId: create_user_users_post requestBody: required: true @@ -99,6 +151,13 @@ paths: content: application/json: schema: {} + "400": + description: Bad Request - Invalid username or role + content: + application/json: + schema: {} + "403": + description: Forbidden - Admin role required "422": description: Validation Error content: @@ -109,7 +168,10 @@ paths: delete: tags: - Auth - summary: Delete User + 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. operationId: delete_user_users__username__delete parameters: - name: username @@ -118,12 +180,15 @@ paths: schema: type: string title: Username + description: The username of the user to delete responses: "200": description: Successful Response content: application/json: schema: {} + "403": + description: Forbidden - Cannot delete admin user or admin role required "422": description: Validation Error content: @@ -134,7 +199,17 @@ paths: put: tags: - Auth - summary: Update Password + 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 meet strength requirements: + - Minimum 8 characters + - At least one uppercase letter + - At least one digit + - At least one special character (!@#$%^&*(),.?":{}\|<>) + + If user changes their own password, a new JWT cookie is automatically issued. operationId: update_password_users__username__password_put parameters: - name: username @@ -143,6 +218,7 @@ paths: schema: type: string title: Username + description: The username of the user whose password to update requestBody: required: true content: @@ -155,6 +231,14 @@ paths: content: application/json: schema: {} + "400": + description: Bad Request - Current password required or password doesn't meet requirements + "401": + description: Unauthorized - Current password is incorrect + "403": + description: Forbidden - Viewers can only update their own password + "404": + description: Not Found - User not found "422": description: Validation Error content: @@ -165,7 +249,10 @@ paths: put: tags: - Auth - summary: Update Role + 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. operationId: update_role_users__username__role_put parameters: - name: username @@ -174,6 +261,7 @@ paths: schema: type: string title: Username + description: The username of the user whose role to update requestBody: required: true content: @@ -186,6 +274,10 @@ paths: content: application/json: schema: {} + "400": + description: Bad Request - Invalid role + "403": + description: Forbidden - Cannot modify admin user's role or admin role required "422": description: Validation Error content: @@ -195,20 +287,31 @@ paths: /faces: get: tags: - - Events - summary: Get Faces + - Classification + 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. operationId: get_faces_faces_get responses: "200": description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/FacesResponse" /faces/reprocess: post: tags: - - Events - summary: Reclassify Face + - Classification + 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. operationId: reclassify_face_faces_reprocess_post requestBody: content: @@ -231,8 +334,15 @@ paths: /faces/train/{name}/classify: post: tags: - - Events - summary: Train Face + - Classification + 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. operationId: train_face_faces_train__name__classify_post parameters: - name: name @@ -252,7 +362,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -262,8 +373,13 @@ paths: /faces/{name}/create: post: tags: - - Events - summary: Create Face + - Classification + 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. operationId: create_face_faces__name__create_post parameters: - name: name @@ -277,7 +393,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -287,8 +404,14 @@ paths: /faces/{name}/register: post: tags: - - Events - summary: Register Face + - Classification + 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. operationId: register_face_faces__name__register_post parameters: - name: name @@ -309,7 +432,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -319,8 +443,12 @@ paths: /faces/recognize: post: tags: - - Events - summary: Recognize Face + - Classification + 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. operationId: recognize_face_faces_recognize_post requestBody: required: true @@ -333,7 +461,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/FaceRecognitionResponse" "422": description: Validation Error content: @@ -343,8 +472,13 @@ paths: /faces/{name}/delete: post: tags: - - Events - summary: Deregister Faces + - Classification + 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. operationId: deregister_faces_faces__name__delete_post parameters: - name: name @@ -354,17 +488,18 @@ paths: type: string title: Name requestBody: + required: true content: application/json: schema: - type: object - title: Body + $ref: "#/components/schemas/DeleteFaceImagesBody" responses: "200": description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -374,8 +509,11 @@ paths: /faces/{old_name}/rename: put: tags: - - Events - summary: Rename Face + - Classification + 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. operationId: rename_face_faces__old_name__rename_put parameters: - name: old_name @@ -395,7 +533,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -405,8 +544,13 @@ paths: /lpr/reprocess: put: tags: - - Events - summary: Reprocess License Plate + - Classification + 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. operationId: reprocess_license_plate_lpr_reprocess_put parameters: - name: event_id @@ -430,15 +574,274 @@ paths: /reindex: put: tags: - - Events - summary: Reindex Embeddings + - Classification + 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. operationId: reindex_embeddings_reindex_put + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + /audio/transcribe: + put: + tags: + - Classification + 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. + 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" + /classification/attributes: + get: + tags: + - Classification + 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. + operationId: get_custom_attributes_classification_attributes_get + parameters: + - name: object_type + in: query + schema: + type: string + - name: group_by_model + in: query + schema: + type: boolean + default: false + responses: + "200": + description: Successful Response + "422": + description: Validation Error + /classification/{name}/dataset: + get: + tags: + - Classification + 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. + 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" + /classification/{name}/train: + get: + tags: + - Classification + 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. + 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" + post: + tags: + - Classification + 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. + 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" + /classification/{name}/dataset/{category}/delete: + post: + tags: + - Classification + summary: Delete classification dataset images + description: >- + Deletes specific dataset images for a given classification model and + category. + 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" + /classification/{name}/dataset/categorize: + post: + tags: + - Classification + summary: Categorize a classification image + description: >- + Categorizes a specific classification image for a given classification + model and category. + 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" + /classification/{name}/train/delete: + post: + tags: + - Classification + 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. + 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" /review: get: tags: @@ -768,6 +1171,39 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /review/summarize/start/{start_ts}/end/{end_ts}: + post: + tags: + - Review + summary: Generate Review Summary + description: Use GenAI to summarize review items over a period of time. + operationId: >- + 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" /: get: tags: @@ -1322,15 +1758,21 @@ paths: get: tags: - Preview - summary: Preview Ts - description: Get all mp4 previews relevant for time period. + 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. operationId: preview_ts_preview__camera_name__start__start_ts__end__end_ts__get parameters: - name: camera_name in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -1349,7 +1791,13 @@ paths: description: Successful Response content: application/json: - schema: {} + 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: @@ -1360,8 +1808,12 @@ paths: get: tags: - Preview - summary: Preview Hour - description: Get all mp4 previews relevant for time period given the timezone + 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). operationId: >- preview_hour_preview__year_month___day___hour___camera_name___tz_name__get parameters: @@ -1387,7 +1839,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: tz_name in: path @@ -1400,7 +1854,13 @@ paths: description: Successful Response content: application/json: - schema: {} + 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: @@ -1411,8 +1871,12 @@ paths: get: tags: - Preview - summary: Get Preview Frames From Cache - description: Get list of cached preview frames + 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. operationId: >- get_preview_frames_from_cache_preview__camera_name__start__start_ts__end__end_ts__frames_get parameters: @@ -1420,7 +1884,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -1439,7 +1905,13 @@ paths: description: Successful Response content: application/json: - schema: {} + 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: @@ -1450,7 +1922,10 @@ paths: get: tags: - Notifications - summary: Get Vapid Pub Key + 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. operationId: get_vapid_pub_key_notifications_pubkey_get responses: "200": @@ -1462,7 +1937,10 @@ paths: post: tags: - Notifications - summary: Register Notifications + summary: Register notifications + description: |- + 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: @@ -1486,19 +1964,31 @@ paths: get: tags: - Export - summary: Get Exports + 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). operationId: get_exports_exports_get responses: "200": description: Successful Response content: application/json: - schema: {} + schema: + type: array + items: + $ref: "#/components/schemas/ExportModel" + title: Response Get Exports Exports Get /export/{camera_name}/start/{start_time}/end/{end_time}: post: tags: - Export - summary: Export Recording + 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. operationId: >- export_recording_export__camera_name__start__start_time__end__end_time__post parameters: @@ -1506,7 +1996,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_time in: path @@ -1531,7 +2023,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/StartExportResponse" "422": description: Validation Error content: @@ -1542,7 +2035,10 @@ paths: patch: tags: - Export - summary: Export Rename + summary: Rename export + description: |- + 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 @@ -1562,7 +2058,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -1573,7 +2070,7 @@ paths: delete: tags: - Export - summary: Export Delete + summary: Delete export operationId: export_delete_export__event_id__delete parameters: - name: event_id @@ -1587,7 +2084,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/GenericResponse" "422": description: Validation Error content: @@ -1598,7 +2096,10 @@ paths: get: tags: - Export - summary: Get Export + summary: Get a single export + description: |- + 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 @@ -1612,7 +2113,8 @@ paths: description: Successful Response content: application/json: - schema: {} + schema: + $ref: "#/components/schemas/ExportModel" "422": description: Validation Error content: @@ -1623,7 +2125,8 @@ paths: get: tags: - Events - summary: Events + summary: Get events + description: Returns a list of events. operationId: events_events_get parameters: - name: camera @@ -1759,10 +2262,6 @@ paths: - name: include_thumbnails in: query required: false - description: > - Deprecated. Thumbnail data is no longer included in the response. - Use the /api/events/:event_id/thumbnail.:extension endpoint instead. - deprecated: true schema: anyOf: - type: integer @@ -1887,7 +2386,10 @@ paths: get: tags: - Events - summary: Events Explore + 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. operationId: events_explore_events_explore_get parameters: - name: limit @@ -1917,7 +2419,10 @@ paths: get: tags: - Events - summary: Event Ids + summary: Get events by ids + description: |- + Gets events by a list of ids. + Returns a list of events. operationId: event_ids_event_ids_get parameters: - name: ids @@ -1946,7 +2451,10 @@ paths: get: tags: - Events - summary: Events Search + summary: Search events + description: |- + Searches for events in the database. + Returns a list of events. operationId: events_search_events_search_get parameters: - name: query @@ -1977,10 +2485,6 @@ paths: - name: include_thumbnails in: query required: false - description: > - Deprecated. Thumbnail data is no longer included in the response. - Use the /api/events/:event_id/thumbnail.:extension endpoint instead. - deprecated: true schema: anyOf: - type: integer @@ -2190,7 +2694,8 @@ paths: get: tags: - Events - summary: Event + summary: Get event by id + description: Gets an event by its id. operationId: event_events__event_id__get parameters: - name: event_id @@ -2215,7 +2720,10 @@ paths: delete: tags: - Events - summary: Delete Event + summary: Delete event + description: |- + 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 @@ -2241,7 +2749,11 @@ paths: post: tags: - Events - summary: Set Retain + 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. operationId: set_retain_events__event_id__retain_post parameters: - name: event_id @@ -2266,7 +2778,11 @@ paths: delete: tags: - Events - summary: Delete Retain + 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. operationId: delete_retain_events__event_id__retain_delete parameters: - name: event_id @@ -2292,7 +2808,10 @@ paths: post: tags: - Events - summary: Send To Plus + summary: Send event to Frigate+ + description: |- + 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 @@ -2323,7 +2842,11 @@ paths: put: tags: - Events - summary: False Positive + 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. operationId: false_positive_events__event_id__false_positive_put parameters: - name: event_id @@ -2349,7 +2872,10 @@ paths: post: tags: - Events - summary: Set Sub Label + 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. operationId: set_sub_label_events__event_id__sub_label_post parameters: - name: event_id @@ -2381,7 +2907,10 @@ paths: post: tags: - Events - summary: Set Plate + 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. operationId: set_plate_events__event_id__recognized_license_plate_post parameters: - name: event_id @@ -2409,11 +2938,50 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/attributes: + post: + tags: + - Events + summary: Set custom classification attributes + description: |- + Sets an event's custom classification attributes for all attribute-type + models that apply to the event's object type. + Returns a success message or an error if the event is not found. + 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" /events/{event_id}/description: post: tags: - Events - summary: Set Description + summary: Set event description + description: |- + 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 @@ -2445,7 +3013,10 @@ paths: put: tags: - Events - summary: Regenerate Description + summary: Regenerate event description + description: |- + 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 @@ -2463,6 +3034,43 @@ paths: - 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" + /description/generate: + post: + tags: + - Events + 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. + operationId: generate_description_embedding_description_generate_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsDescriptionBody" responses: "200": description: Successful Response @@ -2480,7 +3088,10 @@ paths: delete: tags: - Events - summary: Delete Events + 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. operationId: delete_events_events__delete requestBody: required: true @@ -2505,7 +3116,13 @@ paths: post: tags: - Events - summary: Create Event + 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. operationId: create_event_events__camera_name___label__create_post parameters: - name: camera_name @@ -2526,7 +3143,6 @@ paths: schema: $ref: "#/components/schemas/EventsCreateBody" default: - source_type: api score: 0 duration: 30 include_recording: true @@ -2548,7 +3164,11 @@ paths: put: tags: - Events - summary: End Event + 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. operationId: end_event_events__event_id__end_put parameters: - name: event_id @@ -2576,6 +3196,159 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /trigger/embedding: + post: + tags: + - Events + 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. + 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" + /trigger/embedding/{camera_name}/{name}: + put: + tags: + - Events + 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. + 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" + delete: + tags: + - Events + 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. + 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" + /triggers/status/{camera_name}: + get: + tags: + - Events + 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. + 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" /{camera_name}: get: tags: @@ -2587,7 +3360,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: fps in: query @@ -2674,7 +3449,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name responses: "200": @@ -2699,7 +3476,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: extension in: path @@ -2746,6 +3525,14 @@ paths: - 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 @@ -2803,7 +3590,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: frame_time in: path @@ -2849,7 +3638,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: frame_time in: path @@ -2929,7 +3720,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: timezone in: query @@ -2956,7 +3749,7 @@ paths: - Media summary: Recordings description: >- - Return specific camera recordings between the given "after"/"end" times. + 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: @@ -2964,21 +3757,23 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: after in: query required: false schema: type: number - default: 1752611870.43948 + default: 1759932070.40171 title: After - name: before in: query required: false schema: type: number - default: 1752615470.43949 + default: 1759935670.40172 title: Before responses: "200": @@ -2992,6 +3787,56 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /recordings/unavailable: + get: + tags: + - Media + summary: No Recordings + description: 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" /{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4: get: tags: @@ -3006,7 +3851,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -3046,7 +3893,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -3104,7 +3953,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name responses: "200": @@ -3151,7 +4002,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: tz_name in: path @@ -3187,6 +4040,15 @@ paths: 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 @@ -3295,8 +4157,7 @@ paths: in: path required: true schema: - type: string - title: Extension + $ref: "#/components/schemas/Extension" - name: max_cache_age in: query required: false @@ -3339,7 +4200,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: color in: query @@ -3367,7 +4230,7 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" - /events/{event_id}/snapshot-clean.png: + /events/{event_id}/snapshot-clean.webp: get: tags: - Media @@ -3412,6 +4275,15 @@ paths: 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 @@ -3460,7 +4332,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -3506,7 +4380,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: start_ts in: path @@ -3639,7 +4515,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: label in: path @@ -3670,7 +4548,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: label in: path @@ -3701,7 +4581,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: label in: path @@ -3735,7 +4617,9 @@ paths: in: path required: true schema: - type: string + anyOf: + - type: string + - type: "null" title: Camera Name - name: label in: path @@ -3763,6 +4647,16 @@ components: 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 type: object title: AppConfigSetBody AppPostLoginBody: @@ -3815,6 +4709,15 @@ components: required: - role title: AppPutRoleBody + AudioTranscriptionBody: + properties: + event_id: + type: string + title: Event Id + type: object + required: + - event_id + title: AudioTranscriptionBody Body_recognize_face_faces_recognize_post: properties: file: @@ -3861,6 +4764,18 @@ components: - total_alert - total_detection title: DayReview + 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: @@ -4005,12 +4920,6 @@ components: title: EventUploadPlusResponse EventsCreateBody: properties: - source_type: - anyOf: - - type: string - - type: "null" - title: Source Type - default: api sub_label: anyOf: - type: string @@ -4112,6 +5021,59 @@ components: required: - subLabel title: EventsSubLabelBody + EventsAttributesBody: + properties: + attributes: + type: object + title: Attributes + description: Object with model names as keys and attribute values + additionalProperties: + type: string + type: object + required: + - attributes + title: EventsAttributesBody + 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 + type: object + required: + - id + - camera + - name + - date + - video_path + - thumb_path + - in_progress + title: ExportModel + description: Model representing a single export. ExportRecordingsBody: properties: playback: @@ -4149,6 +5111,53 @@ components: - 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"] + } GenericResponse: properties: success: @@ -4204,6 +5213,37 @@ components: - 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: @@ -4306,6 +5346,28 @@ components: - 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 + type: object + required: + - success + - message + title: StartExportResponse + description: Response model for starting an export. SubmitPlusBody: properties: include_annotation: @@ -4314,6 +5376,30 @@ components: default: 1 type: object title: SubmitPlusBody + TriggerEmbeddingBody: + properties: + type: + $ref: "#/components/schemas/TriggerType" + data: + type: string + title: Data + threshold: + type: number + maximum: 1 + minimum: 0 + title: Threshold + default: 0.5 + type: object + required: + - type + - data + title: TriggerEmbeddingBody + TriggerType: + type: string + enum: + - thumbnail + - description + title: TriggerType ValidationError: properties: loc: diff --git a/docs/static/img/branding/LICENSE.md b/docs/static/img/branding/LICENSE.md new file mode 100644 index 000000000..abb0cb350 --- /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/favicon.ico b/docs/static/img/branding/favicon.ico similarity index 100% rename from docs/static/img/favicon.ico rename to docs/static/img/branding/favicon.ico diff --git a/docs/static/img/frigate.png b/docs/static/img/branding/frigate.png similarity index 100% rename from docs/static/img/frigate.png rename to docs/static/img/branding/frigate.png diff --git a/docs/static/img/logo-dark.svg b/docs/static/img/branding/logo-dark.svg similarity index 100% rename from docs/static/img/logo-dark.svg rename to docs/static/img/branding/logo-dark.svg diff --git a/docs/static/img/logo.svg b/docs/static/img/branding/logo.svg similarity index 100% rename from docs/static/img/logo.svg rename to docs/static/img/branding/logo.svg diff --git a/docs/static/img/frigate-autotracking-example.gif b/docs/static/img/frigate-autotracking-example.gif index b0bc424b7..bd6ab390e 100644 Binary files a/docs/static/img/frigate-autotracking-example.gif and b/docs/static/img/frigate-autotracking-example.gif differ diff --git a/frigate/__main__.py b/frigate/__main__.py index 4143f7ae6..f3181e494 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -1,5 +1,6 @@ import argparse import faulthandler +import multiprocessing as mp import signal import sys import threading @@ -15,12 +16,17 @@ from frigate.util.config import find_config_file def main() -> None: + manager = mp.Manager() faulthandler.enable() # Setup the logging thread - setup_logging() + 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()) @@ -93,7 +99,14 @@ def main() -> None: print("*************************************************************") print("*** End Config Validation Errors ***") print("*************************************************************") - sys.exit(1) + + # 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. ***") @@ -101,8 +114,23 @@ def main() -> None: sys.exit(0) # Run the main application. - FrigateApp(config).start() + 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/app.py b/frigate/api/app.py index f6e9471f2..440adfce4 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -6,43 +6,53 @@ import json import logging import os 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, Optional +from typing import Any, Dict, List, Optional import aiofiles -import requests 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 markupsafe import escape -from peewee import SQL, operator +from peewee import SQL, fn, operator from pydantic import ValidationError -from frigate.api.auth import require_role +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 from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, +) from frigate.models import Event, Timeline from frigate.stats.prometheus import get_metrics, update_metrics from frigate.util.builtin import ( clean_camera_user_pass, - get_tz_modifiers, - update_yaml_from_url, + flatten_config_data, + process_config_query_string, + update_yaml_file_bulk, ) from frigate.util.config import find_config_file from frigate.util.services import ( - ffprobe_stream, 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__) @@ -51,66 +61,33 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.app]) -@router.get("/", response_class=PlainTextResponse) +@router.get( + "/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) def is_healthy(): return "Frigate is running. Alive and healthy!" -@router.get("/config/schema.json") +@router.get("/config/schema.json", dependencies=[Depends(allow_public())]) def config_schema(request: Request): return Response( content=request.app.frigate_config.schema_json(), media_type="application/json" ) -@router.get("/go2rtc/streams") -def go2rtc_streams(): - r = 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() - 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/{camera_name}") -def go2rtc_camera_stream(request: Request, camera_name: str): - r = requests.get( - f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone" - ) - if not r.ok: - camera_config = request.app.frigate_config.cameras.get(camera_name) - - 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.get("/version", response_class=PlainTextResponse) +@router.get( + "/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) def version(): return VERSION -@router.get("/stats") +@router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) def stats(request: Request): return JSONResponse(content=request.app.stats_emitter.get_latest_stats()) -@router.get("/stats/history") +@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())]) def stats_history(request: Request, keys: str = None): if keys: keys = keys.split(",") @@ -118,17 +95,24 @@ def stats_history(request: Request, keys: str = None): return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys)) -@router.get("/metrics") +@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() - update_metrics(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("/config") +@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( @@ -204,7 +188,37 @@ def config(request: Request): return JSONResponse(content=config) -@router.get("/config/raw") +@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(allow_any_authenticated())]) def config_raw(): config_file = find_config_file() @@ -354,14 +368,37 @@ def config_set(request: Request, body: AppConfigSetBody): with open(config_file, "r") as f: old_raw_config = f.read() - f.close() try: - update_yaml_from_url(config_file, str(request.url)) + 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: + updates = flatten_config_data(body.config_data) + + 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, "r") as f: new_raw_config = f.read() - f.close() - # Validate the config schema + try: config = FrigateConfig.parse(new_raw_config) except Exception: @@ -385,8 +422,34 @@ def config_set(request: Request, body: AppConfigSetBody): status_code=500, ) - if body.requires_restart == 0: + if body.requires_restart == 0 or body.update_topic: + old_config: FrigateConfig = request.app.frigate_config request.app.frigate_config = config + + if body.update_topic: + if body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + + 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=( { @@ -398,67 +461,7 @@ def config_set(request: Request, body: AppConfigSetBody): ) -@router.get("/ffprobe") -def ffprobe(request: Request, paths: str = ""): - 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()) - output.append( - { - "return_code": ffprobe.returncode, - "stderr": ( - ffprobe.stderr.decode("unicode_escape").strip() - if ffprobe.returncode != 0 - else "" - ), - "stdout": ( - json.loads(ffprobe.stdout.decode("unicode_escape").strip()) - if ffprobe.returncode == 0 - else "" - ), - } - ) - - return JSONResponse(content=output) - - -@router.get("/vainfo") +@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())]) def vainfo(): vainfo = vainfo_hwaccel() return JSONResponse( @@ -478,12 +481,16 @@ def vainfo(): ) -@router.get("/nvinfo") +@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())]) def nvinfo(): return JSONResponse(content=get_nvidia_driver_info()) -@router.get("/logs/{service}", tags=[Tags.logs]) +@router.get( + "/logs/{service}", + tags=[Tags.logs], + dependencies=[Depends(allow_any_authenticated())], +) async def logs( service: str = Path(enum=["frigate", "nginx", "go2rtc"]), download: Optional[str] = None, @@ -591,7 +598,7 @@ def restart(): ) -@router.get("/labels") +@router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) def get_labels(camera: str = ""): try: if camera: @@ -609,7 +616,7 @@ def get_labels(camera: str = ""): return JSONResponse(content=labels) -@router.get("/sub_labels") +@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) def get_sub_labels(split_joined: Optional[int] = None): try: events = Event.select(Event.sub_label).distinct() @@ -640,7 +647,7 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) -@router.get("/plus/models") +@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( @@ -682,14 +689,22 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False): return JSONResponse(content=validModels) -@router.get("/recognized_license_plates") -def get_recognized_license_plates(split_joined: Optional[int] = None): +@router.get( + "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] +) +def get_recognized_license_plates( + split_joined: Optional[int] = None, + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): try: query = ( Event.select( SQL("json_extract(data, '$.recognized_license_plate') AS plate") ) - .where(SQL("json_extract(data, '$.recognized_license_plate') IS NOT NULL")) + .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()] @@ -716,7 +731,7 @@ def get_recognized_license_plates(split_joined: Optional[int] = None): return JSONResponse(content=recognized_license_plates) -@router.get("/timeline") +@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())]) def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): clauses = [] @@ -733,7 +748,11 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N clauses.append((Timeline.camera == camera)) if source_id: - clauses.append((Timeline.source_id == 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))) if len(clauses) == 0: clauses.append((True)) @@ -749,7 +768,7 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N return JSONResponse(content=[t for t in timeline]) -@router.get("/timeline/hourly") +@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()): """Get hourly summary for timeline.""" cameras = params.cameras diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 95586e955..7ba845f45 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -11,7 +11,7 @@ import secrets import time from datetime import datetime from pathlib import Path -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import JSONResponse, RedirectResponse @@ -32,8 +32,178 @@ from frigate.models import User logger = logging.getLogger(__name__) + +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(). + + Port 5000 (internal) 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", + # Public info endpoints (allow_public) + "/", + "/version", + "/config/schema.json", + # Authenticated user endpoints (allow_any_authenticated) + "/metrics", + "/stats", + "/stats/history", + "/config", + "/config/raw", + "/vainfo", + "/nvinfo", + "/labels", + "/sub_labels", + "/plus/models", + "/recognized_license_plates", + "/timeline", + "/timeline/hourly", + "/recordings/storage", + "/recordings/summary", + "/recordings/unavailable", + "/go2rtc/streams", + "/event_ids", + "/events", + "/exports", + } + + # 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 + "/exports/", # /exports/{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 + # Port 5000 (internal) 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 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) + + 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") + if username is None: + raise HTTPException(status_code=401, detail="Authentication required") + return + + return auth_checker + + router = APIRouter(tags=[Tags.auth]) -VALID_ROLES = ["admin", "viewer"] + + +@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: @@ -127,7 +297,10 @@ def get_jwt_secret() -> str: ) jwt_secret = secrets.token_hex(64) try: - with open(jwt_secret_file, "w") as f: + 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( @@ -172,9 +345,35 @@ def verify_password(password, password_hash): return secrets.compare_digest(password_hash, compare_hash) +def validate_password_strength(password: str) -> tuple[bool, Optional[str]]: + """ + Validate password strength. + + Returns a tuple of (is_valid, error_message). + """ + if not password: + return False, "Password cannot be empty" + + if len(password) < 8: + return False, "Password must be at least 8 characters long" + + if not any(c.isupper() for c in password): + return False, "Password must contain at least one uppercase letter" + + if not any(c.isdigit() for c in password): + return False, "Password must contain at least one digit" + + if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password): + return False, "Password must contain at least one special character" + + return True, None + + def create_encoded_jwt(user, role, expiration, secret): return jwt.encode( - {"alg": "HS256"}, {"sub": user, "role": role, "exp": expiration}, secret + {"alg": "HS256"}, + {"sub": user, "role": role, "exp": expiration, "iat": int(time.time())}, + secret, ) @@ -204,6 +403,7 @@ async def get_current_user(request: Request): 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") @@ -217,21 +417,155 @@ def require_role(required_roles: List[str]): if not roles: raise HTTPException(status_code=403, detail="Role not provided") - # Check if any role matches required_roles - if not any(role in required_roles for role in roles): + # 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"Role {', '.join(roles)} not authorized. Required: {', '.join(required_roles)}", + detail=f"No valid roles found in {roles}. Required: {', '.join(required_roles)}. Available: {', '.join(config_roles)}", ) - # Return the first matching role - return next((role for role in roles if role in required_roles), roles[0]) + 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. + - 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 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") +@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 @@ -258,30 +592,19 @@ def auth(request: Request): # 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 anonymous if none are 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="anonymous") + request.headers.get(user_header, default="viewer") if user_header - else "anonymous" + else "viewer" ) - role_header = proxy_config.header_map.role - role = ( - request.headers.get(role_header, default=proxy_config.default_role) - if role_header - else proxy_config.default_role - ) - - # if comma-separated with "admin", use "admin", - # if comma-separated with "viewer", use "viewer", - # else use default role - - roles = [r.strip() for r in role.split(proxy_config.separator)] if role else [] - success_response.headers["remote-role"] = next( - (r for r in VALID_ROLES if r in roles), proxy_config.default_role - ) + # 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 return success_response # now apply authentication @@ -341,13 +664,27 @@ def auth(request: Request): return fail_response # if the jwt cookie is expiring soon - elif jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time: + if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time: logger.debug("jwt token expiring soon, refreshing cookie") - # ensure the user hasn't been deleted + + # Check if password has been changed since token was issued + # If so, force re-login by rejecting the refresh try: - User.get_by_id(user) + 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 @@ -368,15 +705,31 @@ def auth(request: Request): return fail_response -@router.get("/profile") +@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", "anonymous") + username = request.headers.get("remote-user", "viewer") role = request.headers.get("remote-role", "viewer") - return JSONResponse(content={"username": username, "role": 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) + + return JSONResponse( + content={"username": username, "role": role, "allowed_cameras": allowed_cameras} + ) -@router.get("/logout") +@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) @@ -387,7 +740,12 @@ def logout(request: Request): limiter = Limiter(key_func=get_remote_addr) -@router.post("/login") +@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): JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name @@ -404,19 +762,33 @@ def login(request: Request, body: AppPostLoginBody): password_hash = db_user.password_hash if verify_password(password, password_hash): role = getattr(db_user, "role", "viewer") - if role not in VALID_ROLES: - role = "viewer" # Enforce valid roles + 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"]))]) +@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() @@ -424,17 +796,28 @@ def get_users(): return JSONResponse([e for e in exports]) -@router.post("/users", dependencies=[Depends(require_role(["admin"]))]) +@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 meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).', +) 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) - role = body.role if body.role in VALID_ROLES else "viewer" + if body.role not in config_roles: + return JSONResponse( + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, + ) + role = body.role or "viewer" password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) User.insert( { @@ -447,7 +830,12 @@ def create_user( return JSONResponse(content={"username": body.username}) -@router.delete("/users/{username}", dependencies=[Depends(require_role(["admin"]))]) +@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": @@ -459,7 +847,12 @@ def delete_user(request: Request, username: str): return JSONResponse(content={"success": True}) -@router.put("/users/{username}/password") +@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 meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.", +) async def update_password( request: Request, username: str, @@ -481,15 +874,66 @@ async def update_password( HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations - password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) - User.set_by_id(username, {User.password_hash: password_hash}) + try: + user = User.get_by_id(username) + except DoesNotExist: + return JSONResponse(content={"message": "User not found"}, status_code=404) - return JSONResponse(content={"success": True}) + # 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, @@ -511,10 +955,52 @@ async def update_role( return JSONResponse( content={"message": "Cannot modify admin user's role"}, status_code=403 ) - if body.role not in VALID_ROLES: + config_roles = list(request.app.frigate_config.auth.roles.keys()) + if body.role not in config_roles: return JSONResponse( - content={"message": "Role must be 'admin' or 'viewer'"}, status_code=400 + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, ) User.set_by_id(username, {User.role: body.role}) return JSONResponse(content={"success": True}) + + +async def require_camera_access( + camera_name: Optional[str] = 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): + return current_user + + 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 + + if camera_name not in allowed_cameras: + raise HTTPException( + status_code=403, + detail=f"Access denied to camera '{camera_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 000000000..488ec1e1f --- /dev/null +++ b/frigate/api/camera.py @@ -0,0 +1,997 @@ +"""Camera apis.""" + +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 onvif import ONVIFCamera, ONVIFError +from zeep.exceptions import Fault, TransportError +from zeep.transports import AsyncTransport + +from frigate.api.auth import ( + allow_any_authenticated, + require_camera_access, + require_role, +) +from frigate.api.defs.tags import Tags +from frigate.config.config import FrigateConfig +from frigate.util.builtin import clean_camera_user_pass +from frigate.util.image import run_ffmpeg_snapshot +from frigate.util.services import ffprobe_stream + +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())]) +def go2rtc_streams(): + r = 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() + 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/{camera_name}", dependencies=[Depends(require_camera_access)] +) +def go2rtc_camera_stream(request: Request, camera_name: str): + r = requests.get( + f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone" + ) + if not r.ok: + camera_config = request.app.frigate_config.cameras.get(camera_name) + + 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.""" + try: + params = {"name": stream_name} + if src: + params["src"] = 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("/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 + + +@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 = ONVIFCamera( + host, port, username or "", password or "", wsdl_dir=wsdl_base + ) + + # Configure digest authentication if requested + if auth_type == "digest" and username and password: + # Create httpx client with digest auth + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + + # Replace the transport in the zeep client + transport = AsyncTransport(client=client) + + # Update the xaddr before setting transport + await onvif_camera.update_xaddrs() + + # Replace transport in all services + if hasattr(onvif_camera, "devicemgmt"): + onvif_camera.devicemgmt.zeep_client.transport = transport + if hasattr(onvif_camera, "media"): + onvif_camera.media.zeep_client.transport = transport + if hasattr(onvif_camera, "ptz"): + onvif_camera.ptz.zeep_client.transport = transport + + logger.debug("Configured digest authentication") + else: + await onvif_camera.update_xaddrs() + + # 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: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + device_service.zeep_client.transport = transport + + 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: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + media_service.zeep_client.transport = transport + + 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: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + ptz_service.zeep_client.transport = transport + + # 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: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + media_service.zeep_client.transport = transport + + 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 + ) + print(probe) + 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}") diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 21ee59fb6..f60cfd3c3 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -3,7 +3,9 @@ import datetime import logging import os +import random import shutil +import string from typing import Any import cv2 @@ -14,20 +16,48 @@ 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 RenameFaceBody +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.const import FACE_DIR +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.path import get_event_snapshot +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.events]) +router = APIRouter(tags=[Tags.classification]) -@router.get("/faces") +@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]] = {} @@ -51,7 +81,15 @@ def get_faces(): return JSONResponse(status_code=200, content=face_dict) -@router.post("/faces/reprocess", dependencies=[Depends(require_role(["admin"]))]) +@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( @@ -78,13 +116,32 @@ def reclassify_face(request: Request, body: dict = None): 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, - status_code=200, ) -@router.post("/faces/train/{name}/classify") +@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( @@ -123,8 +180,7 @@ def train_face(request: Request, name: str, body: dict = None): new_name = f"{sanitized_name}-{datetime.datetime.now().timestamp()}.webp" new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}") - if not os.path.exists(new_file_folder): - os.mkdir(new_file_folder) + os.makedirs(new_file_folder, exist_ok=True) if training_file_name: shutil.move(training_file, os.path.join(new_file_folder, new_name)) @@ -188,7 +244,16 @@ def train_face(request: Request, name: str, body: dict = None): ) -@router.post("/faces/{name}/create", dependencies=[Depends(require_role(["admin"]))]) +@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( @@ -205,7 +270,16 @@ async def create_face(request: Request, name: str): ) -@router.post("/faces/{name}/register", dependencies=[Depends(require_role(["admin"]))]) +@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.""", +) async def register_face(request: Request, name: str, file: UploadFile): if not request.app.frigate_config.face_recognition.enabled: return JSONResponse( @@ -231,7 +305,14 @@ async def register_face(request: Request, name: str, file: UploadFile): ) -@router.post("/faces/recognize") +@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.""", +) async def recognize_face(request: Request, file: UploadFile): if not request.app.frigate_config.face_recognition.enabled: return JSONResponse( @@ -257,28 +338,38 @@ async def recognize_face(request: Request, file: UploadFile): ) -@router.post("/faces/{name}/delete", dependencies=[Depends(require_role(["admin"]))]) -def deregister_faces(request: Request, name: str, body: dict = None): +@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}, ) - json: dict[str, Any] = body or {} - list_of_ids = json.get("ids", "") - context: EmbeddingsContext = request.app.embeddings - context.delete_face_ids( - name, map(lambda file: sanitize_filename(file), list_of_ids) - ) + 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", dependencies=[Depends(require_role(["admin"]))]) +@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( @@ -307,7 +398,14 @@ def rename_face(request: Request, old_name: str, body: RenameFaceBody): ) -@router.put("/lpr/reprocess") +@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." @@ -340,7 +438,14 @@ def reprocess_license_plate(request: Request, event_id: str): ) -@router.put("/reindex", dependencies=[Depends(require_role(["admin"]))]) +@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 = ( @@ -384,3 +489,602 @@ def reindex_embeddings(request: Request): }, 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) + ) + + 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) + + if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none": + os.rmdir(folder) + + return JSONResponse( + content=({"success": True, "message": "Successfully deleted images."}), + 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/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 187dd3f91..8e5a5391a 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -12,6 +12,7 @@ class EventsQueryParams(BaseModel): labels: Optional[str] = "all" sub_label: Optional[str] = "all" sub_labels: Optional[str] = "all" + attributes: Optional[str] = "all" zone: Optional[str] = "all" zones: Optional[str] = "all" limit: Optional[int] = 100 @@ -58,6 +59,8 @@ class EventsSearchQueryParams(BaseModel): limit: Optional[int] = 50 cameras: Optional[str] = "all" labels: Optional[str] = "all" + sub_labels: Optional[str] = "all" + attributes: Optional[str] = "all" zones: Optional[str] = "all" after: Optional[float] = None before: Optional[float] = None diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py index 8ab799a56..a16f0d53f 100644 --- a/frigate/api/defs/query/media_query_parameters.py +++ b/frigate/api/defs/query/media_query_parameters.py @@ -1,7 +1,8 @@ from enum import Enum -from typing import Optional +from typing import Optional, Union from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema class Extension(str, Enum): @@ -22,6 +23,7 @@ class MediaLatestFrameQueryParams(BaseModel): zones: Optional[int] = None mask: Optional[int] = None motion: Optional[int] = None + paths: Optional[int] = None regions: Optional[int] = None quality: Optional[int] = 70 height: Optional[int] = None @@ -51,3 +53,10 @@ class MediaMjpegFeedQueryParams(BaseModel): class MediaRecordingsSummaryQueryParams(BaseModel): timezone: str = "utc" cameras: Optional[str] = "all" + + +class MediaRecordingsAvailabilityQueryParams(BaseModel): + cameras: str = "all" + before: Union[float, SkipJsonSchema[None]] = None + after: Union[float, SkipJsonSchema[None]] = None + scale: int = 30 diff --git a/frigate/api/defs/query/regenerate_query_parameters.py b/frigate/api/defs/query/regenerate_query_parameters.py index bcce47b1b..af50ada2c 100644 --- a/frigate/api/defs/query/regenerate_query_parameters.py +++ b/frigate/api/defs/query/regenerate_query_parameters.py @@ -1,9 +1,13 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field from frigate.events.types import RegenerateDescriptionEnum class RegenerateQueryParameters(BaseModel): source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails + force: Optional[bool] = Field( + default=False, + description="Force (re)generating the description even if GenAI is disabled for this camera.", + ) diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py index ee9af740e..6d01d824d 100644 --- a/frigate/api/defs/query/review_query_parameters.py +++ b/frigate/api/defs/query/review_query_parameters.py @@ -10,7 +10,7 @@ class ReviewQueryParams(BaseModel): cameras: str = "all" labels: str = "all" zones: str = "all" - reviewed: int = 0 + reviewed: Union[int, SkipJsonSchema[None]] = None limit: Union[int, SkipJsonSchema[None]] = None severity: Union[SeverityEnum, SkipJsonSchema[None]] = None before: Union[float, SkipJsonSchema[None]] = None diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 1fc05db2f..c4129d8da 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -1,14 +1,17 @@ -from typing import Optional +from typing import Any, Dict, Optional from pydantic import BaseModel class AppConfigSetBody(BaseModel): requires_restart: int = 1 + update_topic: str | None = None + config_data: Optional[Dict[str, Any]] = None class AppPutPasswordBody(BaseModel): password: str + old_password: Optional[str] = None class AppPostUsersBody(BaseModel): diff --git a/frigate/api/defs/request/classification_body.py b/frigate/api/defs/request/classification_body.py index c4a32c332..fb6a7dd0f 100644 --- a/frigate/api/defs/request/classification_body.py +++ b/frigate/api/defs/request/classification_body.py @@ -1,5 +1,31 @@ -from pydantic import BaseModel +from typing import Dict, List, Tuple + +from pydantic import BaseModel, Field class RenameFaceBody(BaseModel): - new_name: str + 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 index 0883d066f..50754e92a 100644 --- a/frigate/api/defs/request/events_body.py +++ b/frigate/api/defs/request/events_body.py @@ -2,6 +2,8 @@ from typing import List, Optional, Union from pydantic import BaseModel, Field +from frigate.config.classification import TriggerType + class EventsSubLabelBody(BaseModel): subLabel: str = Field(title="Sub label", max_length=100) @@ -22,12 +24,18 @@ class EventsLPRBody(BaseModel): ) +class EventsAttributesBody(BaseModel): + attributes: List[str] = Field( + title="Selected classification attributes for the event", + default_factory=list, + ) + + class EventsDescriptionBody(BaseModel): description: Union[str, None] = Field(title="The description of the event") class EventsCreateBody(BaseModel): - source_type: Optional[str] = "api" sub_label: Optional[str] = None score: Optional[float] = 0 duration: Optional[int] = 30 @@ -45,3 +53,9 @@ class EventsDeleteBody(BaseModel): 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_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py index eb6c15155..19fc2f019 100644 --- a/frigate/api/defs/request/export_recordings_body.py +++ b/frigate/api/defs/request/export_recordings_body.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema @@ -16,5 +16,5 @@ class ExportRecordingsBody(BaseModel): source: PlaybackSourceEnum = Field( default=PlaybackSourceEnum.recordings, title="Playback source" ) - name: str = Field(title="Friendly name", default=None, max_length=256) + name: Optional[str] = Field(title="Friendly name", default=None, max_length=256) image_path: Union[str, SkipJsonSchema[None]] = None diff --git a/frigate/api/defs/request/review_body.py b/frigate/api/defs/request/review_body.py index 991f190f8..6dc710035 100644 --- a/frigate/api/defs/request/review_body.py +++ b/frigate/api/defs/request/review_body.py @@ -4,3 +4,5 @@ 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/classification_response.py b/frigate/api/defs/response/classification_response.py new file mode 100644 index 000000000..92d354f24 --- /dev/null +++ b/frigate/api/defs/response/classification_response.py @@ -0,0 +1,38 @@ +from typing import Dict, List, Optional + +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: Optional[float] = Field( + default=None, description="Confidence score of the recognition (0-1)" + ) + face_name: Optional[str] = Field( + default=None, description="The recognized face name if successful" + ) diff --git a/frigate/api/defs/response/export_response.py b/frigate/api/defs/response/export_response.py new file mode 100644 index 000000000..63a9e91a1 --- /dev/null +++ b/frigate/api/defs/response/export_response.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +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" + ) + + +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: Optional[str] = Field( + default=None, description="The export ID if successfully started" + ) + + +ExportsResponse = List[ExportModel] diff --git a/frigate/api/defs/response/preview_response.py b/frigate/api/defs/response/preview_response.py new file mode 100644 index 000000000..d320a865d --- /dev/null +++ b/frigate/api/defs/response/preview_response.py @@ -0,0 +1,17 @@ +from typing import List + +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/tags.py b/frigate/api/defs/tags.py index 9e61da9e9..f804385d1 100644 --- a/frigate/api/defs/tags.py +++ b/frigate/api/defs/tags.py @@ -3,6 +3,7 @@ from enum import Enum class Tags(Enum): app = "App" + camera = "Camera" preview = "Preview" logs = "Logs" media = "Media" @@ -10,5 +11,5 @@ class Tags(Enum): review = "Review" export = "Export" events = "Events" - classification = "classification" + classification = "Classification" auth = "Auth" diff --git a/frigate/api/event.py b/frigate/api/event.py index 27353e4b5..ea5cfb29c 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -1,22 +1,32 @@ """Event apis.""" +import base64 import datetime +import json import logging import os import random import string from functools import reduce from pathlib import Path +from typing import List from urllib.parse import unquote import cv2 +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 require_role +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, @@ -27,6 +37,7 @@ from frigate.api.defs.query.regenerate_query_parameters import ( RegenerateQueryParameters, ) from frigate.api.defs.request.events_body import ( + EventsAttributesBody, EventsCreateBody, EventsDeleteBody, EventsDescriptionBody, @@ -34,6 +45,7 @@ from frigate.api.defs.request.events_body import ( EventsLPRBody, EventsSubLabelBody, SubmitPlusBody, + TriggerEmbeddingBody, ) from frigate.api.defs.response.event_response import ( EventCreateResponse, @@ -44,19 +56,30 @@ from frigate.api.defs.response.event_response import ( 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.const import CLIPS_DIR +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 +from frigate.models import Event, ReviewSegment, Timeline, Trigger from frigate.track.object_processing import TrackedObject -from frigate.util.builtin import get_tz_modifiers +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.time import get_dst_transitions, get_tz_modifiers logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.events]) -@router.get("/events", response_model=list[EventResponse]) -def events(params: EventsQueryParams = Depends()): +@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 @@ -78,6 +101,8 @@ def events(params: EventsQueryParams = Depends()): if sub_labels == "all" and sub_label != "all": sub_labels = sub_label + attributes = unquote(params.attributes) + zone = params.zone zones = params.zones @@ -130,8 +155,14 @@ def events(params: EventsQueryParams = Depends()): clauses.append((Event.camera == camera)) if cameras != "all": - camera_list = cameras.split(",") - clauses.append((Event.camera << camera_list)) + 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(",") @@ -160,44 +191,44 @@ def events(params: EventsQueryParams = Depends()): 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 + filtered_attributes = attributes.split(",") + attribute_clauses = [] + + for attr in filtered_attributes: + attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') + + attribute_clause = reduce(operator.or_, attribute_clauses) + clauses.append(attribute_clause) + if recognized_license_plate != "all": - # use matching so joined recognized_license_plates are included - # for example a recognized license plate 'ABC123' would get events - # with recognized license plates 'ABC123' and 'ABC123, XYZ789' - recognized_license_plate_clauses = [] filtered_recognized_license_plates = recognized_license_plate.split(",") + clauses_for_plates = [] + if "None" in filtered_recognized_license_plates: filtered_recognized_license_plates.remove("None") - recognized_license_plate_clauses.append( - (Event.data["recognized_license_plate"].is_null()) + 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) ) - for recognized_license_plate in filtered_recognized_license_plates: - # Exact matching plus list inclusion - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - == recognized_license_plate - ) - ) - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - % f"*{recognized_license_plate},*" - ) - ) - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - % f"*, {recognized_license_plate}*" - ) - ) - - recognized_license_plate_clause = reduce( - operator.or_, recognized_license_plate_clauses - ) - clauses.append((recognized_license_plate_clause)) + 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 @@ -326,10 +357,26 @@ def events(params: EventsQueryParams = Depends()): return JSONResponse(content=list(events)) -@router.get("/events/explore", response_model=list[EventResponse]) -def events_explore(limit: int = 10): +@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).distinct().order_by(Event.label) + distinct_labels = ( + Event.select(Event.label) + .where(Event.camera << allowed_cameras) + .distinct() + .order_by(Event.label) + ) label_counts = {} @@ -340,14 +387,18 @@ def events_explore(limit: int = 10): # get most recent events for this label label_events = ( Event.select() - .where(Event.label == label) + .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).count() + label_counts[label] = ( + Event.select() + .where((Event.label == label) & (Event.camera << allowed_cameras)) + .count() + ) yield from label_events @@ -399,8 +450,16 @@ def events_explore(limit: int = 10): return JSONResponse(content=processed_events) -@router.get("/event_ids", response_model=list[EventResponse]) -def event_ids(ids: str): +@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: @@ -409,6 +468,14 @@ def event_ids(ids: str): 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)) @@ -418,8 +485,19 @@ def event_ids(ids: str): ) -@router.get("/events/search") -def events_search(request: Request, params: EventsSearchQueryParams = Depends()): +@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 @@ -429,6 +507,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) # Filters cameras = params.cameras labels = params.labels + sub_labels = params.sub_labels + attributes = params.attributes zones = params.zones after = params.after before = params.before @@ -492,11 +572,49 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters = [] if cameras != "all": - event_filters.append((Event.camera << cameras.split(","))) + 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: + sub_label_clauses.append( + (Event.sub_label.cast("text") == label) + ) # include exact matches + + # include this label when part of a list + sub_label_clauses.append((Event.sub_label.cast("text") % f"*{label},*")) + sub_label_clauses.append((Event.sub_label.cast("text") % f"*, {label}*")) + + event_filters.append((reduce(operator.or_, sub_label_clauses))) + + if attributes != "all": + # Custom classification results are stored as data[model_name] = result_value + filtered_attributes = attributes.split(",") + attribute_clauses = [] + + for attr in filtered_attributes: + attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') + + event_filters.append(reduce(operator.or_, attribute_clauses)) + if zones != "all": zone_clauses = [] filtered_zones = zones.split(",") @@ -511,42 +629,31 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters.append((reduce(operator.or_, zone_clauses))) if recognized_license_plate != "all": - # use matching so joined recognized_license_plates are included - # for example an recognized_license_plate 'ABC123' would get events - # with recognized_license_plates 'ABC123' and 'ABC123, XYZ789' - recognized_license_plate_clauses = [] filtered_recognized_license_plates = recognized_license_plate.split(",") + clauses_for_plates = [] + if "None" in filtered_recognized_license_plates: filtered_recognized_license_plates.remove("None") - recognized_license_plate_clauses.append( - (Event.data["recognized_license_plate"].is_null()) + 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) ) - for recognized_license_plate in filtered_recognized_license_plates: - # Exact matching plus list inclusion - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - == recognized_license_plate - ) - ) - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - % f"*{recognized_license_plate},*" - ) - ) - recognized_license_plate_clauses.append( - ( - Event.data["recognized_license_plate"].cast("text") - % f"*, {recognized_license_plate}*" - ) - ) - - recognized_license_plate_clause = reduce( - operator.or_, recognized_license_plate_clauses - ) + recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) event_filters.append((recognized_license_plate_clause)) if after: @@ -755,10 +862,12 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) return JSONResponse(content=processed_events) -@router.get("/events/summary") -def events_summary(params: EventsSummaryQueryParams = Depends()): +@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 - hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name) has_clip = params.has_clip has_snapshot = params.has_snapshot @@ -773,39 +882,105 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): if len(clauses) == 0: clauses.append((True)) - groups = ( + time_range_query = ( Event.select( - Event.camera, - Event.label, - Event.sub_label, - Event.data, - fn.strftime( - "%Y-%m-%d", - fn.datetime( - Event.start_time, "unixepoch", hour_modifier, minute_modifier - ), - ).alias("day"), - Event.zones, - fn.COUNT(Event.id).alias("count"), - ) - .where(reduce(operator.and_, clauses)) - .group_by( - Event.camera, - Event.label, - Event.sub_label, - Event.data, - (Event.start_time + seconds_offset).cast("int") / (3600 * 24), - Event.zones, + 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() ) - return JSONResponse(content=[e for e in groups.dicts()]) + 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) -def event(event_id: str): +@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: - return model_to_dict(Event.get(Event.id == event_id)) + 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) @@ -814,6 +989,11 @@ def event(event_id: str): "/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: @@ -833,8 +1013,16 @@ def set_retain(event_id: str): ) -@router.post("/events/{event_id}/plus", response_model=EventUploadPlusResponse) -def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): +@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) @@ -852,6 +1040,7 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = 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) @@ -864,12 +1053,12 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): include_annotation = None if event.end_time is None: - logger.error(f"Unable to load clean png for in-progress event: {event.id}") + logger.error(f"Unable to load clean snapshot for in-progress event: {event.id}") return JSONResponse( content=( { "success": False, - "message": "Unable to load clean png for in-progress event", + "message": "Unable to load clean snapshot for in-progress event", } ), status_code=400, @@ -882,24 +1071,44 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): content=({"success": False, "message": message}), status_code=400 ) - # load clean.png + # load clean.webp or clean.png (legacy) try: - filename = f"{event.camera}-{event.id}-clean.png" - image = cv2.imread(os.path.join(CLIPS_DIR, filename)) + filename_webp = f"{event.camera}-{event.id}-clean.webp" + filename_png = f"{event.camera}-{event.id}-clean.png" + + image_path = None + if os.path.exists(os.path.join(CLIPS_DIR, filename_webp)): + image_path = os.path.join(CLIPS_DIR, filename_webp) + elif os.path.exists(os.path.join(CLIPS_DIR, filename_png)): + image_path = os.path.join(CLIPS_DIR, filename_png) + + if image_path is None: + 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, + ) + + image = cv2.imread(image_path) except Exception: - logger.error(f"Unable to load clean png for event: {event.id}") + logger.error(f"Unable to load clean snapshot for event: {event.id}") return JSONResponse( content=( - {"success": False, "message": "Unable to load clean png for event"} + {"success": False, "message": "Unable to load clean snapshot for event"} ), status_code=400, ) if image is None or image.size == 0: - logger.error(f"Unable to load clean png for event: {event.id}") + logger.error(f"Unable to load clean snapshot for event: {event.id}") return JSONResponse( content=( - {"success": False, "message": "Unable to load clean png for event"} + {"success": False, "message": "Unable to load clean snapshot for event"} ), status_code=400, ) @@ -945,8 +1154,16 @@ def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): ) -@router.put("/events/{event_id}/false_positive", response_model=EventUploadPlusResponse) -def false_positive(request: Request, event_id: str): +@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) @@ -962,6 +1179,7 @@ def false_positive(request: Request, event_id: str): 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) @@ -985,7 +1203,7 @@ def false_positive(request: Request, event_id: str): ) if not event.plus_id: - plus_response = send_to_plus(request, event_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 @@ -1038,10 +1256,16 @@ def false_positive(request: Request, event_id: str): "/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. + """, ) -def delete_retain(event_id: str): +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"}), @@ -1061,14 +1285,19 @@ def delete_retain(event_id: str): "/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. + """, ) -def set_sub_label( +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 @@ -1099,7 +1328,7 @@ def set_sub_label( new_score = None request.app.event_metadata_updater.publish( - EventMetadataTypeEnum.sub_label, (event_id, new_sub_label, new_score) + (event_id, new_sub_label, new_score), EventMetadataTypeEnum.sub_label.value ) return JSONResponse( @@ -1115,14 +1344,19 @@ def set_sub_label( "/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. + """, ) -def set_plate( +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 @@ -1153,7 +1387,8 @@ def set_plate( new_score = None request.app.event_metadata_updater.publish( - EventMetadataTypeEnum.recognized_license_plate, (event_id, new_plate, new_score) + (event_id, "recognized_license_plate", new_plate, new_score), + EventMetadataTypeEnum.attribute.value, ) return JSONResponse( @@ -1165,18 +1400,124 @@ def set_plate( ) +@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. + """, ) -def set_description( +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"}), @@ -1220,12 +1561,17 @@ def set_description( "/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. + """, ) -def regenerate_description( +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"}), @@ -1234,9 +1580,10 @@ def regenerate_description( camera_config = request.app.frigate_config.cameras[event.camera] - if camera_config.genai.enabled: + if camera_config.objects.genai.enabled or params.force: request.app.event_metadata_updater.publish( - EventMetadataTypeEnum.regenerate_description, (event.id, params.source) + (event.id, params.source, params.force), + EventMetadataTypeEnum.regenerate_description.value, ) return JSONResponse( @@ -1263,9 +1610,46 @@ def regenerate_description( ) -def delete_single_event(event_id: str, request: Request) -> dict: +@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"} @@ -1274,6 +1658,7 @@ def delete_single_event(event_id: str, request: Request) -> dict: 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) @@ -1294,9 +1679,13 @@ def delete_single_event(event_id: str, request: Request) -> dict: "/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. + """, ) -def delete_event(request: Request, event_id: str): - result = delete_single_event(event_id, request) +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) @@ -1305,8 +1694,12 @@ def delete_event(request: Request, event_id: str): "/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. + """, ) -def delete_events(request: Request, body: EventsDeleteBody): +async def delete_events(request: Request, body: EventsDeleteBody): if not body.event_ids: return JSONResponse( content=({"success": False, "message": "No event IDs provided."}), @@ -1317,7 +1710,7 @@ def delete_events(request: Request, body: EventsDeleteBody): not_found_events = [] for event_id in body.event_ids: - result = delete_single_event(event_id, request) + result = await delete_single_event(event_id, request) if result["success"]: deleted_events.append(event_id) else: @@ -1335,6 +1728,13 @@ def delete_events(request: Request, body: EventsDeleteBody): "/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, @@ -1361,7 +1761,6 @@ def create_event( event_id = f"{now}-{rand_id}" request.app.event_metadata_updater.publish( - EventMetadataTypeEnum.manual_event_create, ( now, camera_name, @@ -1371,9 +1770,10 @@ def create_event( body.score, body.sub_label, body.duration, - body.source_type, + "api", body.draw, ), + EventMetadataTypeEnum.manual_event_create.value, ) return JSONResponse( @@ -1392,12 +1792,36 @@ def create_event( "/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. + """, ) -def end_event(request: Request, event_id: str, body: EventsEndBody): +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( - EventMetadataTypeEnum.manual_event_end, (event_id, end_time) + (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( @@ -1411,3 +1835,447 @@ def end_event(request: Request, event_id: str, body: EventsEndBody): 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 index 44ec05c15..24fed93b0 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -4,6 +4,7 @@ import logging import random import string from pathlib import Path +from typing import List import psutil from fastapi import APIRouter, Depends, Request @@ -12,9 +13,20 @@ from pathvalidate import sanitize_filepath from peewee import DoesNotExist from playhouse.shortcuts import model_to_dict -from frigate.api.auth import require_role +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, + require_role, +) from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody from frigate.api.defs.request.export_rename_body import ExportRenameBody +from frigate.api.defs.response.export_response import ( + 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.models import Export, Previews, Recordings @@ -23,20 +35,44 @@ from frigate.record.export import ( PlaybackSourceEnum, RecordingExporter, ) -from frigate.util.builtin import is_current_hour +from frigate.util.time import is_current_hour logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.export]) -@router.get("/exports") -def get_exports(): - exports = Export.select().order_by(Export.date.desc()).dicts().iterator() +@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), +): + exports = ( + Export.select() + .where(Export.camera << allowed_cameras) + .order_by(Export.date.desc()) + .dicts() + .iterator() + ) return JSONResponse(content=[e for e in exports]) -@router.post("/export/{camera_name}/start/{start_time}/end/{end_time}") +@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, @@ -140,11 +176,18 @@ def export_recording( @router.patch( - "/export/{event_id}/rename", dependencies=[Depends(require_role(["admin"]))] + "/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. + """, ) -def export_rename(event_id: str, body: ExportRenameBody): +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=( @@ -169,10 +212,16 @@ def export_rename(event_id: str, body: ExportRenameBody): ) -@router.delete("/export/{event_id}", dependencies=[Depends(require_role(["admin"]))]) -def export_delete(event_id: str): +@router.delete( + "/export/{event_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete export", +) +async def export_delete(event_id: str, request: Request): try: export: Export = Export.get(Export.id == event_id) + await require_camera_access(export.camera, request=request) except DoesNotExist: return JSONResponse( content=( @@ -222,10 +271,19 @@ def export_delete(event_id: str): ) -@router.get("/exports/{export_id}") -def get_export(export_id: str): +@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: - return JSONResponse(content=model_to_dict(Export.get(Export.id == export_id))) + 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"}, diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 0657752dc..48c97dfaf 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,8 +1,10 @@ import logging +import re from typing import Optional -from fastapi import FastAPI, Request +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 @@ -13,6 +15,7 @@ from starlette_context.plugins import Plugin from frigate.api import app as main_app from frigate.api import ( auth, + camera, classification, event, export, @@ -21,11 +24,12 @@ from frigate.api import ( preview, review, ) -from frigate.api.auth import get_jwt_secret, limiter +from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default from frigate.comms.event_metadata_updater import ( EventMetadataPublisher, ) from frigate.config import FrigateConfig +from frigate.config.camera.updater import CameraConfigUpdatePublisher from frigate.embeddings import EmbeddingsContext from frigate.ptz.onvif import OnvifController from frigate.stats.emitter import StatsEmitter @@ -57,11 +61,16 @@ def create_fastapi_app( onvif: OnvifController, stats_emitter: StatsEmitter, event_metadata_updater: EventMetadataPublisher, + config_publisher: CameraConfigUpdatePublisher, + 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 @@ -110,6 +119,7 @@ def create_fastapi_app( # 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(classification.router) app.include_router(review.router) app.include_router(main_app.router) @@ -127,6 +137,27 @@ def create_fastapi_app( app.onvif = onvif app.stats_emitter = stats_emitter app.event_metadata_updater = event_metadata_updater - app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None + app.config_publisher = config_publisher + + 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 index e3de57cd3..971bfef83 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -8,25 +8,31 @@ import os import subprocess as sp import time from datetime import datetime, timedelta, timezone +from functools import reduce from pathlib import Path as FilePath -from typing import Any +from typing import Any, List from urllib.parse import unquote import cv2 import numpy as np import pytz -from fastapi import APIRouter, Path, Query, Request, Response -from fastapi.params import Depends +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 peewee import DoesNotExist, fn, operator from tzlocal import get_localzone_name +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) from frigate.api.defs.query.media_query_parameters import ( Extension, MediaEventsSnapshotQueryParams, MediaLatestFrameQueryParams, MediaMjpegFeedQueryParams, + MediaRecordingsAvailabilityQueryParams, MediaRecordingsSummaryQueryParams, ) from frigate.api.defs.tags import Tags @@ -42,18 +48,17 @@ from frigate.const import ( ) from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment from frigate.track.object_processing import TrackedObjectProcessor -from frigate.util.builtin import get_tz_modifiers +from frigate.util.file import get_event_thumbnail_bytes from frigate.util.image import get_image_from_recording -from frigate.util.path import get_event_thumbnail_bytes +from frigate.util.time import get_dst_transitions logger = logging.getLogger(__name__) - router = APIRouter(tags=[Tags.media]) -@router.get("/{camera_name}") -def mjpeg_feed( +@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)]) +async def mjpeg_feed( request: Request, camera_name: str, params: MediaMjpegFeedQueryParams = Depends(), @@ -109,7 +114,7 @@ def imagestream( ) -@router.get("/{camera_name}/ptz/info") +@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 @@ -125,8 +130,10 @@ async def camera_ptz_info(request: Request, camera_name: str): ) -@router.get("/{camera_name}/latest.{extension}") -def latest_frame( +@router.get( + "/{camera_name}/latest.{extension}", dependencies=[Depends(require_camera_access)] +) +async def latest_frame( request: Request, camera_name: str, extension: Extension, @@ -139,6 +146,7 @@ def latest_frame( "zones": params.zones, "mask": params.mask, "motion_boxes": params.motion, + "paths": params.paths, "regions": params.regions, } quality = params.quality @@ -233,8 +241,11 @@ def latest_frame( ) -@router.get("/{camera_name}/recordings/{frame_time}/snapshot.{format}") -def get_snapshot_from_recording( +@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, @@ -320,8 +331,10 @@ def get_snapshot_from_recording( ) -@router.post("/{camera_name}/plus/{frame_time}") -def submit_recording_snapshot_to_plus( +@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: @@ -384,7 +397,7 @@ def submit_recording_snapshot_to_plus( ) -@router.get("/recordings/storage") +@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())]) def get_recordings_storage_usage(request: Request): recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ "storage" @@ -408,112 +421,196 @@ def get_recordings_storage_usage(request: Request): return JSONResponse(content=camera_usages) -@router.get("/recordings/summary") -def all_recordings_summary(params: MediaRecordingsSummaryQueryParams = Depends()): +@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""" - hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(params.timezone) 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 - query = ( + time_range_query = ( Recordings.select( - fn.strftime( - "%Y-%m-%d", - fn.datetime( - Recordings.start_time + seconds_offset, - "unixepoch", - hour_modifier, - minute_modifier, - ), - ).alias("day") + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), ) - .group_by( - fn.strftime( - "%Y-%m-%d", - fn.datetime( - Recordings.start_time + seconds_offset, - "unixepoch", - hour_modifier, - minute_modifier, - ), - ) - ) - .order_by(Recordings.start_time.desc()) + .where(Recordings.camera << camera_list) + .dicts() + .get() ) - if cameras != "all": - query = query.where(Recordings.camera << cameras.split(",")) + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") - recording_days = query.namedtuples() - days = {day.day: True for day in recording_days} + if min_time is None or max_time is None: + return JSONResponse(content={}) - return JSONResponse(content=days) + 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: + 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_query = ( + Recordings.select( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day") + ) + .where( + (Recordings.camera << camera_list) + & (Recordings.end_time >= period_start) + & (Recordings.start_time <= period_end) + ) + .group_by( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ) + ) + .order_by(Recordings.start_time.desc()) + .namedtuples() + ) + + for g in period_query: + days[g.day] = True + + return JSONResponse(content=dict(sorted(days.items()))) -@router.get("/{camera_name}/recordings/summary") -def recordings_summary(camera_name: str, timezone: str = "utc"): +@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""" - hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(timezone) - recording_groups = ( + + time_range_query = ( Recordings.select( - fn.strftime( - "%Y-%m-%d %H", - fn.datetime( - Recordings.start_time, "unixepoch", hour_modifier, minute_modifier - ), - ).alias("hour"), - fn.SUM(Recordings.duration).alias("duration"), - fn.SUM(Recordings.motion).alias("motion"), - fn.SUM(Recordings.objects).alias("objects"), + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), ) .where(Recordings.camera == camera_name) - .group_by((Recordings.start_time + seconds_offset).cast("int") / 3600) - .order_by(Recordings.start_time.desc()) - .namedtuples() + .dicts() + .get() ) - event_groups = ( - Event.select( - fn.strftime( - "%Y-%m-%d %H", - fn.datetime( - Event.start_time, "unixepoch", hour_modifier, minute_modifier - ), - ).alias("hour"), - fn.COUNT(Event.id).alias("count"), + 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() ) - .where(Event.camera == camera_name, Event.has_clip) - .group_by((Event.start_time + seconds_offset).cast("int") / 3600) - .namedtuples() - ) - event_map = {g.hour: g.count for g in event_groups} + 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() + ) - days = {} + 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 not in days: - days[day] = {"events": events_count, "hours": [hour_data], "day": day} - else: - days[day]["events"] += events_count - days[day]["hours"].append(hour_data) + 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") -def recordings( +@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(), @@ -542,11 +639,97 @@ def recordings( 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(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + cameras = ",".join(filtered) + else: + cameras = allowed_cameras + + 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)] + if cameras != "all": + camera_list = cameras.split(",") + clauses.append((Recordings.camera << camera_list)) + else: + camera_list = allowed_cameras + + # 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 + recordings = [(r["start_time"], r["end_time"]) for r in data] + + # Iterate through time segments and check if each has any recording + no_recording_segments = [] + current = after + current_gap_start = None + + while current < before: + segment_end = min(current + scale, before) + + # Check if this segment overlaps with any recording + has_recording = any( + rec_start < segment_end and rec_end > current + for rec_start, rec_end in recordings + ) + + 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.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.", ) -def recording_clip( +async def recording_clip( request: Request, camera_name: str, start_ts: float, @@ -587,6 +770,15 @@ def recording_clip( .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: @@ -642,9 +834,22 @@ def recording_clip( @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.", ) -def vod_ts(camera_name: str, start_ts: float, end_ts: float): +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, @@ -664,10 +869,19 @@ def vod_ts(camera_name: str, start_ts: float, end_ts: float): 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) @@ -676,19 +890,35 @@ def vod_ts(camera_name: str, start_ts: float, end_ts: float): 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) - if duration <= 0: - # skip if the clip has no valid duration + 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 0 < duration < max_duration_ms: + 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}") @@ -708,7 +938,7 @@ def vod_ts(camera_name: str, start_ts: float, end_ts: float): return JSONResponse( content={ "cache": hour_ago.timestamp() > start_ts, - "discontinuity": False, + "discontinuity": force_discontinuity, "consistentSequenceMediaInfo": True, "durations": durations, "segment_duration": max(durations), @@ -719,20 +949,24 @@ def vod_ts(camera_name: str, start_ts: float, end_ts: float): @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.", ) -def vod_hour_no_timezone(year_month: str, day: int, hour: int, camera_name: str): +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 vod_hour( + 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.", ) -def vod_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: str): +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=timezone.utc) @@ -742,14 +976,16 @@ def vod_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: st start_ts = start_date.timestamp() end_ts = end_date.timestamp() - return vod_ts(camera_name, start_ts, end_ts) + 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.", ) -def vod_event( +async def vod_event( + request: Request, event_id: str, padding: int = Query(0, description="Padding to apply to the vod."), ): @@ -765,22 +1001,14 @@ def vod_event( status_code=404, ) - if not event.has_clip: - logger.error(f"Event does not have recordings: {event_id}") - return JSONResponse( - content={ - "success": False, - "message": "Recordings not available.", - }, - 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 = vod_ts(event.camera, event.start_time - padding, end_ts) + 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 ( @@ -794,11 +1022,24 @@ def vod_event( 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. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.", ) -def event_snapshot( +async def event_snapshot( request: Request, event_id: str, params: MediaEventsSnapshotQueryParams = Depends(), @@ -808,6 +1049,7 @@ def event_snapshot( 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"}, @@ -836,6 +1078,7 @@ def event_snapshot( height=params.height, quality=params.quality, ) + await require_camera_access(camera_state.name, request=request) except Exception: return JSONResponse( content={"success": False, "message": "Ongoing event not found"}, @@ -868,8 +1111,11 @@ def event_snapshot( ) -@router.get("/events/{event_id}/thumbnail.{extension}") -def event_thumbnail( +@router.get( + "/events/{event_id}/thumbnail.{extension}", + dependencies=[Depends(require_camera_access)], +) +async def event_thumbnail( request: Request, event_id: str, extension: Extension, @@ -882,6 +1128,7 @@ def event_thumbnail( 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 @@ -944,7 +1191,7 @@ def event_thumbnail( ) -@router.get("/{camera_name}/grid.jpg") +@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 ): @@ -1065,9 +1312,12 @@ def grid_snapshot( ) -@router.get("/events/{event_id}/snapshot-clean.png") +@router.get( + "/events/{event_id}/snapshot-clean.webp", + dependencies=[Depends(require_camera_access)], +) def event_snapshot_clean(request: Request, event_id: str, download: bool = False): - png_bytes = None + webp_bytes = None try: event = Event.get(Event.id == event_id) snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots @@ -1089,7 +1339,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False if event_id in camera_state.tracked_objects: tracked_obj = camera_state.tracked_objects.get(event_id) if tracked_obj is not None: - png_bytes = tracked_obj.get_clean_png() + webp_bytes = tracked_obj.get_clean_webp() break except Exception: return JSONResponse( @@ -1105,12 +1355,56 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False return JSONResponse( content={"success": False, "message": "Event not found"}, status_code=404 ) - if png_bytes is None: + if webp_bytes is None: try: - clean_snapshot_path = os.path.join( + # webp + clean_snapshot_path_webp = os.path.join( + CLIPS_DIR, f"{event.camera}-{event.id}-clean.webp" + ) + # png (legacy) + clean_snapshot_path_png = os.path.join( CLIPS_DIR, f"{event.camera}-{event.id}-clean.png" ) - if not os.path.exists(clean_snapshot_path): + + if os.path.exists(clean_snapshot_path_webp): + with open(clean_snapshot_path_webp, "rb") as image_file: + webp_bytes = image_file.read() + elif os.path.exists(clean_snapshot_path_png): + # convert png to webp and save for future use + png_image = cv2.imread(clean_snapshot_path_png, cv2.IMREAD_UNCHANGED) + if png_image is None: + return JSONResponse( + content={ + "success": False, + "message": "Invalid png snapshot", + }, + status_code=400, + ) + + ret, webp_data = cv2.imencode( + ".webp", png_image, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + if not ret: + return JSONResponse( + content={ + "success": False, + "message": "Unable to convert png to webp", + }, + status_code=400, + ) + + webp_bytes = webp_data.tobytes() + + # save the converted webp for future requests + try: + with open(clean_snapshot_path_webp, "wb") as f: + f.write(webp_bytes) + except Exception as e: + logger.warning( + f"Failed to save converted webp for event {event.id}: {e}" + ) + # continue since we now have the data to return + else: return JSONResponse( content={ "success": False, @@ -1118,39 +1412,37 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False }, status_code=404, ) - with open( - os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}-clean.png"), "rb" - ) as image_file: - png_bytes = image_file.read() except Exception: - logger.error(f"Unable to load clean png for event: {event.id}") + logger.error(f"Unable to load clean snapshot for event: {event.id}") return JSONResponse( content={ "success": False, - "message": "Unable to load clean png for event", + "message": "Unable to load clean snapshot for event", }, status_code=400, ) headers = { - "Content-Type": "image/png", + "Content-Type": "image/webp", "Cache-Control": "private, max-age=31536000", } if download: headers["Content-Disposition"] = ( - f"attachment; filename=snapshot-{event_id}-clean.png" + f"attachment; filename=snapshot-{event_id}-clean.webp" ) return Response( - png_bytes, - media_type="image/png", + webp_bytes, + media_type="image/webp", headers=headers, ) -@router.get("/events/{event_id}/clip.mp4") -def event_clip( +@router.get( + "/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)] +) +async def event_clip( request: Request, event_id: str, padding: int = Query(0, description="Padding to apply to clip."), @@ -1172,10 +1464,14 @@ def event_clip( if event.end_time is None else event.end_time + padding ) - return recording_clip(request, event.camera, event.start_time - padding, end_ts) + return await recording_clip( + request, event.camera, event.start_time - padding, end_ts + ) -@router.get("/events/{event_id}/preview.gif") +@router.get( + "/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)] +) def event_preview(request: Request, event_id: str): try: event: Event = Event.get(Event.id == event_id) @@ -1191,7 +1487,10 @@ def event_preview(request: Request, event_id: str): return preview_gif(request, event.camera, start_ts, end_ts) -@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif") +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif", + dependencies=[Depends(require_camera_access)], +) def preview_gif( request: Request, camera_name: str, @@ -1347,7 +1646,10 @@ def preview_gif( ) -@router.get("/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4") +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4", + dependencies=[Depends(require_camera_access)], +) def preview_mp4( request: Request, camera_name: str, @@ -1522,7 +1824,7 @@ def preview_mp4( ) -@router.get("/review/{event_id}/preview") +@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)]) def review_preview( request: Request, event_id: str, @@ -1548,8 +1850,12 @@ def review_preview( return preview_mp4(request, review.camera, start_ts, end_ts) -@router.get("/preview/{file_name}/thumbnail.jpg") -@router.get("/preview/{file_name}/thumbnail.webp") +@router.get( + "/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)] +) +@router.get( + "/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)] +) def preview_thumbnail(file_name: str): """Get a thumbnail from the cached preview frames.""" if len(file_name) > 1000: @@ -1587,9 +1893,14 @@ def preview_thumbnail(file_name: str): ####################### dynamic routes ########################### -@router.get("/{camera_name}/{label}/best.jpg") -@router.get("/{camera_name}/{label}/thumbnail.jpg") -def label_thumbnail(request: Request, camera_name: str, label: str): +@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": @@ -1598,7 +1909,7 @@ def label_thumbnail(request: Request, camera_name: str, label: str): try: event_id = event_query.scalar() - return event_thumbnail(request, event_id, Extension.jpg, 60) + 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]) @@ -1610,8 +1921,10 @@ def label_thumbnail(request: Request, camera_name: str, label: str): ) -@router.get("/{camera_name}/{label}/clip.mp4") -def label_clip(request: Request, camera_name: str, label: str): +@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 @@ -1622,15 +1935,17 @@ def label_clip(request: Request, camera_name: str, label: str): try: event = event_query.get() - return event_clip(request, event.id) + 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") -def label_snapshot(request: Request, camera_name: str, label: str): +@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": @@ -1651,7 +1966,7 @@ def label_snapshot(request: Request, camera_name: str, label: str): try: event: Event = event_query.get() - return event_snapshot(request, event.id, MediaEventsSnapshotQueryParams()) + 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]) diff --git a/frigate/api/notification.py b/frigate/api/notification.py index 96ba96fdc..502e76dbd 100644 --- a/frigate/api/notification.py +++ b/frigate/api/notification.py @@ -5,11 +5,12 @@ import os from typing import Any from cryptography.hazmat.primitives import serialization -from fastapi import APIRouter, Request +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 @@ -19,7 +20,14 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.notifications]) -@router.get("/notifications/pubkey") +@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 @@ -39,7 +47,14 @@ def get_vapid_pub_key(request: Request): return JSONResponse(content=utils.b64urlencode(raw_pub), status_code=200) -@router.post("/notifications/register") +@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 diff --git a/frigate/api/preview.py b/frigate/api/preview.py index 2db2326ab..a8fef2044 100644 --- a/frigate/api/preview.py +++ b/frigate/api/preview.py @@ -5,9 +5,18 @@ import os from datetime import datetime, timedelta, timezone import pytz -from fastapi import APIRouter +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 @@ -18,13 +27,35 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.preview]) -@router.get("/preview/{camera_name}/start/{start_ts}/end/{end_ts}") -def preview_ts(camera_name: str, start_ts: float, end_ts: float): +@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": - camera_clause = Previews.camera == camera_name + if camera_name not in allowed_cameras: + raise HTTPException(status_code=403, detail="Access denied for camera") + camera_list = [camera_name] else: - camera_clause = True + camera_list = allowed_cameras + + if not camera_list: + return JSONResponse( + content={"success": False, "message": "No previews found."}, + status_code=404, + ) previews = ( Previews.select( @@ -39,7 +70,7 @@ def preview_ts(camera_name: str, start_ts: float, end_ts: float): | Previews.end_time.between(start_ts, end_ts) | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) ) - .where(camera_clause) + .where(Previews.camera << camera_list) .order_by(Previews.start_time.asc()) .dicts() .iterator() @@ -71,8 +102,24 @@ def preview_ts(camera_name: str, start_ts: float, end_ts: float): return JSONResponse(content=clips, status_code=200) -@router.get("/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}") -def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: str): +@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 = ( @@ -83,10 +130,18 @@ def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name start_ts = start_date.timestamp() end_ts = end_date.timestamp() - return preview_ts(camera_name, start_ts, end_ts) + return preview_ts(camera_name, start_ts, end_ts, allowed_cameras) -@router.get("/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames") +@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") diff --git a/frigate/api/review.py b/frigate/api/review.py index e6d010db7..76619dcb2 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -4,15 +4,22 @@ import datetime import logging from functools import reduce from pathlib import Path +from typing import List import pandas as pd -from fastapi import APIRouter +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 get_current_user, require_role +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, @@ -26,19 +33,26 @@ from frigate.api.defs.response.review_response import ( ReviewSummaryResponse, ) from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.embeddings import EmbeddingsContext from frigate.models import Recordings, ReviewSegment, UserReviewStatus from frigate.review.types import SeverityEnum -from frigate.util.builtin import get_tz_modifiers +from frigate.util.time import get_dst_transitions logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.review]) -@router.get("/review", response_model=list[ReviewSegmentResponse]) +@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 @@ -63,8 +77,14 @@ async def review( ] if cameras != "all": - camera_list = cameras.split(",") - clauses.append((ReviewSegment.camera << camera_list)) + 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 @@ -124,6 +144,8 @@ async def review( (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 = ( @@ -137,8 +159,12 @@ async def review( return JSONResponse(content=[r for r in review_query]) -@router.get("/review_ids", response_model=list[ReviewSegmentResponse]) -def review_ids(ids: str): +@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: @@ -147,6 +173,18 @@ def review_ids(ids: str): 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() @@ -159,17 +197,21 @@ def review_ids(ids: str): ) -@router.get("/review/summary", response_model=ReviewSummaryResponse) +@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"] - hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(params.timezone) day_ago = (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() cameras = params.cameras @@ -179,8 +221,14 @@ async def review_summary( clauses = [(ReviewSegment.start_time > day_ago)] if cameras != "all": - camera_list = cameras.split(",") - clauses.append((ReviewSegment.camera << camera_list)) + 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 @@ -274,8 +322,14 @@ async def review_summary( clauses = [] if cameras != "all": - camera_list = cameras.split(",") - clauses.append((ReviewSegment.camera << camera_list)) + 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 @@ -289,95 +343,146 @@ async def review_summary( ) clauses.append(reduce(operator.or_, label_clauses)) - day_in_seconds = 60 * 60 * 24 - last_month_query = ( + # Find the time range of available data + time_range_query = ( ReviewSegment.select( - fn.strftime( - "%Y-%m-%d", - fn.datetime( - ReviewSegment.start_time, - "unixepoch", - hour_modifier, - 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) - ), + 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) - .group_by( - (ReviewSegment.start_time + seconds_offset).cast("int") / day_in_seconds - ) - .order_by(ReviewSegment.start_time.desc()) + .dicts() + .get() ) + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + data = { "last24Hours": last_24_query, } - for e in last_month_query.dicts().iterator(): - data[e["day"]] = e + # 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) +@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), ): @@ -388,26 +493,33 @@ async def set_multiple_reviewed( 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, ) - # If it exists and isn’t reviewed, update it - if not review_status.has_been_reviewed: - review_status.has_been_reviewed = True + # 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=True, + has_been_reviewed=body.reviewed, ) except (DoesNotExist, IntegrityError): pass return JSONResponse( - content=({"success": True, "message": "Reviewed multiple items"}), + content=( + { + "success": True, + "message": f"Marked multiple items as {'reviewed' if body.reviewed else 'unreviewed'}", + } + ), status_code=200, ) @@ -467,9 +579,14 @@ def delete_reviews(body: ReviewModifyMultipleBody): @router.get( - "/review/activity/motion", response_model=list[ReviewActivityMotionResponse] + "/review/activity/motion", + response_model=list[ReviewActivityMotionResponse], + dependencies=[Depends(allow_any_authenticated())], ) -def motion_activity(params: ReviewActivityMotionQueryParams = Depends()): +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() @@ -484,8 +601,14 @@ def motion_activity(params: ReviewActivityMotionQueryParams = Depends()): clauses.append((Recordings.motion > 0)) if cameras != "all": - camera_list = cameras.split(",") + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) clauses.append((Recordings.camera << camera_list)) + else: + clauses.append((Recordings.camera << allowed_cameras)) data: list[Recordings] = ( Recordings.select( @@ -542,16 +665,18 @@ def motion_activity(params: ReviewActivityMotionQueryParams = Depends()): return JSONResponse(content=normalized) -@router.get("/review/event/{event_id}", response_model=ReviewSegmentResponse) -def get_review_from_event(event_id: str): +@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: - return JSONResponse( - model_to_dict( - ReviewSegment.get( - ReviewSegment.data["detections"].cast("text") % f'*"{event_id}"*' - ) - ) + 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"}, @@ -559,12 +684,16 @@ def get_review_from_event(event_id: str): ) -@router.get("/review/{review_id}", response_model=ReviewSegmentResponse) -def get_review(review_id: str): +@router.get( + "/review/{review_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def get_review(request: Request, review_id: str): try: - return JSONResponse( - content=model_to_dict(ReviewSegment.get(ReviewSegment.id == review_id)) - ) + 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"}, @@ -572,7 +701,11 @@ def get_review(review_id: str): ) -@router.delete("/review/{review_id}/viewed", response_model=GenericResponse) +@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), @@ -606,3 +739,36 @@ async def set_not_reviewed( 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(allow_any_authenticated())], + description="Use GenAI to summarize review items over a period of time.", +) +def generate_review_summary(request: Request, start_ts: float, end_ts: float): + config: FrigateConfig = request.app.frigate_config + + if not config.genai.provider: + 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 index abcefdc56..fac7a08d9 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -5,6 +5,7 @@ import os import secrets import shutil from multiprocessing import Queue +from multiprocessing.managers import DictProxy, SyncManager from multiprocessing.synchronize import Event as MpEvent from pathlib import Path from typing import Optional @@ -14,19 +15,20 @@ import uvicorn from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase -import frigate.util as util 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.config_updater import ConfigPublisher 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.const import ( CACHE_DIR, @@ -36,12 +38,12 @@ from frigate.const import ( FACE_DIR, MODEL_CACHE_DIR, RECORD_DIR, - SHM_FRAMES_VAR, THUMB_DIR, + TRIGGER_DIR, ) from frigate.data_processing.types import DataProcessorMetrics from frigate.db.sqlitevecq import SqliteVecQueueDatabase -from frigate.embeddings import EmbeddingsContext, manage_embeddings +from frigate.embeddings import EmbeddingProcess, EmbeddingsContext from frigate.events.audio import AudioProcessor from frigate.events.cleanup import EventCleanup from frigate.events.maintainer import EventProcessor @@ -55,56 +57,62 @@ from frigate.models import ( Regions, ReviewSegment, Timeline, + Trigger, User, ) from frigate.object_detection.base import ObjectDetectProcess -from frigate.output.output import output_frames +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 manage_recordings -from frigate.review.review import manage_review_segments +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 SharedMemoryFrameManager, UntrackedSharedMemory -from frigate.util.object import get_camera_regions_grid +from frigate.util.image import UntrackedSharedMemory from frigate.util.services import set_file_limit from frigate.version import VERSION -from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog logger = logging.getLogger(__name__) class FrigateApp: - def __init__(self, config: FrigateConfig) -> None: + def __init__( + self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent + ) -> None: + self.metrics_manager = manager self.audio_process: Optional[mp.Process] = None - self.stop_event: MpEvent = mp.Event() + self.stop_event = stop_event self.detection_queue: Queue = mp.Queue() self.detectors: dict[str, ObjectDetectProcess] = {} - self.detection_out_events: dict[str, MpEvent] = {} self.detection_shms: list[mp.shared_memory.SharedMemory] = [] self.log_queue: Queue = mp.Queue() - self.camera_metrics: dict[str, CameraMetrics] = {} + self.camera_metrics: DictProxy = self.metrics_manager.dict() self.embeddings_metrics: DataProcessorMetrics | None = ( - DataProcessorMetrics() + 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: Optional[EmbeddingsContext] = None - self.region_grids: dict[str, list[list[dict[str, int]]]] = {} - self.frame_manager = SharedMemoryFrameManager() self.config = config def ensure_dirs(self) -> None: @@ -121,6 +129,9 @@ class FrigateApp: 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}") @@ -131,7 +142,7 @@ class FrigateApp: def init_camera_metrics(self) -> None: # create camera_metrics for camera_name in self.config.cameras.keys(): - self.camera_metrics[camera_name] = CameraMetrics() + self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager) self.ptz_metrics[camera_name] = PTZMetrics( autotracker_enabled=self.config.cameras[ camera_name @@ -140,8 +151,16 @@ class FrigateApp: 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 for camera in self.config.cameras.values()) * 2 + maxsize=( + sum( + camera.enabled_in_config == True + for camera in self.config.cameras.values() + ) + + 2 + ) + * 2 ) # Queue for timeline events @@ -217,52 +236,24 @@ class FrigateApp: self.processes["go2rtc"] = proc.info["pid"] def init_recording_manager(self) -> None: - recording_process = util.Process( - target=manage_recordings, - name="recording_manager", - args=(self.config,), - ) - recording_process.daemon = True + 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 = util.Process( - target=manage_review_segments, - name="review_segment_manager", - args=(self.config,), - ) - review_segment_process.daemon = True + 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: - genai_cameras = [ - c for c in self.config.cameras.values() if c.enabled and c.genai.enabled - ] - - if ( - not self.config.semantic_search.enabled - and not genai_cameras - and not self.config.lpr.enabled - and not self.config.face_recognition.enabled - and not self.config.classification.bird.enabled - ): - return - - embedding_process = util.Process( - target=manage_embeddings, - name="embeddings_manager", - args=( - self.config, - self.embeddings_metrics, - ), + # always start the embeddings process + embedding_process = EmbeddingProcess( + self.config, self.embeddings_metrics, self.stop_event ) - embedding_process.daemon = True self.embedding_process = embedding_process embedding_process.start() self.processes["embeddings"] = embedding_process.pid or 0 @@ -279,7 +270,9 @@ class FrigateApp: "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]) + 60, + 10 + * len([c for c in self.config.cameras.values() if c.enabled_in_config]), ), load_vec_extension=self.config.semantic_search.enabled, ) @@ -293,6 +286,7 @@ class FrigateApp: ReviewSegment, Timeline, User, + Trigger, ] self.db.bind(models) @@ -308,24 +302,15 @@ class FrigateApp: migrate_exports(self.config.ffmpeg, list(self.config.cameras.keys())) def init_embeddings_client(self) -> None: - genai_cameras = [ - c for c in self.config.cameras.values() if c.enabled and c.genai.enabled - ] - - if ( - self.config.semantic_search.enabled - or self.config.lpr.enabled - or genai_cameras - or self.config.face_recognition.enabled - ): - # Create a client for other processes to use - self.embeddings = EmbeddingsContext(self.db) + # 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 = ConfigPublisher() + 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) @@ -358,8 +343,6 @@ class FrigateApp: def start_detectors(self) -> None: for name in self.config.cameras.keys(): - self.detection_out_events[name] = mp.Event() - try: largest_frame = max( [ @@ -391,8 +374,10 @@ class FrigateApp: self.detectors[name] = ObjectDetectProcess( name, self.detection_queue, - self.detection_out_events, + list(self.config.cameras.keys()), + self.config, detector_config, + self.stop_event, ) def start_ptz_autotracker(self) -> None: @@ -416,79 +401,22 @@ class FrigateApp: self.detected_frames_processor.start() def start_video_output_processor(self) -> None: - output_processor = util.Process( - target=output_frames, - name="output_processor", - args=(self.config,), - ) - output_processor.daemon = True + 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 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 start_camera_processors(self) -> None: - for name, config in self.config.cameras.items(): - if not self.config.cameras[name].enabled_in_config: - logger.info(f"Camera processor not started for disabled camera {name}") - continue - - camera_process = util.Process( - target=track_camera, - name=f"camera_processor:{name}", - args=( - name, - config, - self.config.model, - self.config.model.merged_labelmap, - self.detection_queue, - self.detection_out_events[name], - self.detected_frames_queue, - self.camera_metrics[name], - self.ptz_metrics[name], - self.region_grids[name], - ), - daemon=True, - ) - self.camera_metrics[name].process = camera_process - camera_process.start() - logger.info(f"Camera processor started for {name}: {camera_process.pid}") - - def start_camera_capture_processes(self) -> None: - shm_frame_count = self.shm_frame_count() - - for name, config in self.config.cameras.items(): - if not self.config.cameras[name].enabled_in_config: - logger.info(f"Capture process not started for disabled camera {name}") - continue - - # pre-create shms - for i in range(shm_frame_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 = util.Process( - target=capture_camera, - name=f"camera_capture:{name}", - args=(name, config, shm_frame_count, self.camera_metrics[name]), - ) - capture_process.daemon = True - self.camera_metrics[name].capture_process = capture_process - capture_process.start() - logger.info(f"Capture process started for {name}: {capture_process.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: audio_cameras = [ @@ -498,7 +426,9 @@ class FrigateApp: ] if audio_cameras: - self.audio_process = AudioProcessor(audio_cameras, self.camera_metrics) + self.audio_process = AudioProcessor( + self.config, audio_cameras, self.camera_metrics, self.stop_event + ) self.audio_process.start() self.processes["audio_detector"] = self.audio_process.pid or 0 @@ -546,45 +476,6 @@ class FrigateApp: self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) self.frigate_watchdog.start() - def shm_frame_count(self) -> int: - total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) - - # required for log files + nginx cache - min_req_shm = 40 + 10 - - if self.config.birdseye.restream: - min_req_shm += 8 - - available_shm = total_shm - min_req_shm - cam_total_frame_size = 0.0 - - for camera in self.config.cameras.values(): - if camera.enabled and camera.detect.width and camera.detect.height: - cam_total_frame_size += round( - (camera.detect.width * camera.detect.height * 1.5 + 270480) - / 1048576, - 1, - ) - - if cam_total_frame_size == 0.0: - return 0 - - shm_frame_count = min( - int(os.environ.get(SHM_FRAMES_VAR, "50")), - int(available_shm / (cam_total_frame_size)), - ) - - logger.debug( - f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {shm_frame_count} frames for each camera in SHM" - ) - - if shm_frame_count < 20: - logger.warning( - f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size * 20)}MB." - ) - - return shm_frame_count - def init_auth(self) -> None: if self.config.auth.enabled: if User.select().count() == 0: @@ -601,6 +492,8 @@ class FrigateApp: } ).execute() + self.config.auth.admin_first_time_login = True + logger.info("********************************************************") logger.info("********************************************************") logger.info("*** Auth is enabled, but no users exist. ***") @@ -645,19 +538,17 @@ class FrigateApp: self.init_recording_manager() self.init_review_segment_manager() self.init_go2rtc() - self.start_detectors() self.init_embeddings_manager() self.bind_database() self.check_db_data_migrations() self.init_inter_process_communicator() + self.start_detectors() self.init_dispatcher() self.init_embeddings_client() self.start_video_output_processor() self.start_ptz_autotracker() - self.init_historical_regions() self.start_detected_frames_processor() - self.start_camera_processors() - self.start_camera_capture_processes() + self.start_camera_processor() self.start_audio_processor() self.start_storage_maintainer() self.start_stats_emitter() @@ -680,6 +571,7 @@ class FrigateApp: self.onvif_controller, self.stats_emitter, self.event_metadata_updater, + self.inter_config_updater, ), host="127.0.0.1", port=5001, @@ -713,24 +605,6 @@ class FrigateApp: if self.onvif_controller: self.onvif_controller.close() - # ensure the capture processes are done - for camera, metrics in self.camera_metrics.items(): - capture_process = metrics.capture_process - if capture_process is not None: - logger.info(f"Waiting for capture process for {camera} to stop") - capture_process.terminate() - capture_process.join() - - # ensure the camera processors are done - for camera, metrics in self.camera_metrics.items(): - camera_process = metrics.process - if camera_process is not None: - logger.info(f"Waiting for process for {camera} to stop") - camera_process.terminate() - camera_process.join() - logger.info(f"Closing frame queue for {camera}") - empty_and_close_queue(metrics.frame_queue) - # ensure the detectors are done for detector in self.detectors.values(): detector.stop() @@ -774,14 +648,12 @@ class FrigateApp: self.inter_config_updater.stop() self.event_metadata_updater.stop() self.inter_zmq_proxy.stop() + self.detection_proxy.stop() - self.frame_manager.cleanup() while len(self.detection_shms) > 0: shm = self.detection_shms.pop() shm.close() shm.unlink() - # exit the mp Manager process _stop_logging() - - os._exit(os.EX_OK) + self.metrics_manager.shutdown() diff --git a/frigate/camera/__init__.py b/frigate/camera/__init__.py index 456751c52..77b1fd424 100644 --- a/frigate/camera/__init__.py +++ b/frigate/camera/__init__.py @@ -1,7 +1,7 @@ import multiprocessing as mp +from multiprocessing.managers import SyncManager from multiprocessing.sharedctypes import Synchronized from multiprocessing.synchronize import Event -from typing import Optional class CameraMetrics: @@ -16,25 +16,25 @@ class CameraMetrics: frame_queue: mp.Queue - process: Optional[mp.Process] - capture_process: Optional[mp.Process] + process_pid: Synchronized + capture_process_pid: Synchronized ffmpeg_pid: Synchronized - def __init__(self): - self.camera_fps = mp.Value("d", 0) - self.detection_fps = mp.Value("d", 0) - self.detection_frame = mp.Value("d", 0) - self.process_fps = mp.Value("d", 0) - self.skipped_fps = mp.Value("d", 0) - self.read_start = mp.Value("d", 0) - self.audio_rms = mp.Value("d", 0) - self.audio_dBFS = mp.Value("d", 0) + 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 = mp.Queue(maxsize=2) + self.frame_queue = manager.Queue(maxsize=2) - self.process = None - self.capture_process = None - self.ffmpeg_pid = mp.Value("i", 0) + self.process_pid = manager.Value("i", 0) + self.capture_process_pid = manager.Value("i", 0) + self.ffmpeg_pid = manager.Value("i", 0) class PTZMetrics: diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index 6039a07f6..c2dfa891d 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -1,9 +1,20 @@ """Manage camera activity and updating listeners.""" +import datetime +import json +import logging +import random +import string from collections import Counter from typing import Any, Callable -from frigate.config.config import FrigateConfig +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.config import CameraConfig, FrigateConfig + +logger = logging.getLogger(__name__) class CameraActivityManager: @@ -23,26 +34,33 @@ class CameraActivityManager: if not camera_config.enabled_in_config: continue - 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() + self.__init_camera(camera_config) - 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() + def __init_camera(self, camera_config: CameraConfig) -> None: + 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() - self.all_zone_labels[zone].update( - zone_config.objects - if zone_config.objects - else camera_config.objects.track - ) + 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 update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: all_objects: list[dict[str, Any]] = [] for camera in new_activity.keys(): + # 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) @@ -132,3 +150,110 @@ class CameraActivityManager: if any_changed: self.publish(f"{camera}/all", sum(list(all_objects.values()))) self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) + + +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: + 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(): + # 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 + ) -> None: + max_not_heard = self.config.cameras[camera].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", + {}, + ), + 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 000000000..815e650e9 --- /dev/null +++ b/frigate/camera/maintainer.py @@ -0,0 +1,225 @@ +"""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.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, + ], + ) + self.shm_count = self.__calculate_shm_frame_count() + self.camera_processes: dict[str, mp.Process] = {} + self.capture_processes: dict[str, mp.Process] = {} + self.metrics_manager = metrics_manager + + 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 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 + + 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], + self.stop_event, + self.config.logger, + ) + self.camera_processes[config.name] = camera_process + camera_process.start() + self.camera_metrics[config.name].process_pid.value = camera_process.pid + logger.info(f"Camera processor started for {config.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 + + # 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], + self.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[camera] + if capture_process is not None: + logger.info(f"Waiting for capture process for {camera} to stop") + capture_process.terminate() + capture_process.join() + + def __stop_camera_process(self, camera: str) -> None: + camera_process = self.camera_processes[camera] + if camera_process is not None: + logger.info(f"Waiting for process for {camera} to stop") + 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): + 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: + 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: + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + + # ensure the capture processes are done + for camera in self.camera_processes.keys(): + self.__stop_camera_capture_process(camera) + + # ensure the camera processors are done + for camera in self.capture_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 index 06564bce2..97c715388 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -54,7 +54,7 @@ class CameraState: self.ptz_autotracker_thread = ptz_autotracker_thread self.prev_enabled = self.camera_config.enabled - def get_current_frame(self, draw_options: dict[str, Any] = {}): + 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 @@ -228,12 +228,51 @@ class CameraState: 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): del self.tracked_objects[obj_id] - def on(self, event_type: str, callback: Callable[[dict], None]): + def on(self, event_type: str, callback: Callable): self.callbacks[event_type].append(callback) def update( @@ -491,17 +530,19 @@ class CameraState: # write clean snapshot if enabled if self.camera_config.snapshots.clean_copy: - ret, png = cv2.imencode(".png", img_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.camera_config.name}-{event_id}-clean.png", + f"{self.camera_config.name}-{event_id}-clean.webp", ), "wb", ) as p: - p.write(png.tobytes()) + p.write(webp.tobytes()) # write jpg snapshot with optional annotations if draw.get("boxes") and isinstance(draw.get("boxes"), list): diff --git a/frigate/comms/config_updater.py b/frigate/comms/config_updater.py index 06b870c62..447089a94 100644 --- a/frigate/comms/config_updater.py +++ b/frigate/comms/config_updater.py @@ -1,8 +1,9 @@ """Facilitates communication between processes.""" import multiprocessing as mp +from _pickle import UnpicklingError from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any import zmq @@ -32,7 +33,7 @@ class ConfigPublisher: class ConfigSubscriber: """Simplifies receiving an updated config.""" - def __init__(self, topic: str, exact=False) -> None: + def __init__(self, topic: str, exact: bool = False) -> None: self.topic = topic self.exact = exact self.context = zmq.Context() @@ -40,7 +41,7 @@ class ConfigSubscriber: self.socket.setsockopt_string(zmq.SUBSCRIBE, topic) self.socket.connect(SOCKET_PUB_SUB) - def check_for_update(self) -> Optional[tuple[str, Any]]: + 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) @@ -50,7 +51,7 @@ class ConfigSubscriber: return (topic, obj) else: return (None, None) - except zmq.ZMQError: + except (zmq.ZMQError, UnicodeDecodeError, UnpicklingError): return (None, None) def stop(self) -> None: diff --git a/frigate/comms/detections_updater.py b/frigate/comms/detections_updater.py index 1718d1347..dff61c8a2 100644 --- a/frigate/comms/detections_updater.py +++ b/frigate/comms/detections_updater.py @@ -1,7 +1,7 @@ """Facilitates communication between processes.""" from enum import Enum -from typing import Any, Optional +from typing import Any from .zmq_proxy import Publisher, Subscriber @@ -19,8 +19,7 @@ class DetectionPublisher(Publisher): topic_base = "detection/" - def __init__(self, topic: DetectionTypeEnum) -> None: - topic = topic.value + def __init__(self, topic: str) -> None: super().__init__(topic) @@ -29,16 +28,15 @@ class DetectionSubscriber(Subscriber): topic_base = "detection/" - def __init__(self, topic: DetectionTypeEnum) -> None: - topic = topic.value + def __init__(self, topic: str) -> None: super().__init__(topic) def check_for_update( - self, timeout: float = None - ) -> Optional[tuple[DetectionTypeEnum, Any]]: + 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 (DetectionTypeEnum[topic[len(self.topic_base) :]], payload) + return (topic[len(self.topic_base) :], payload) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 87891ec88..6e45ac175 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -3,24 +3,33 @@ import datetime import json import logging -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, cast from frigate.camera import PTZMetrics -from frigate.camera.activity_manager import CameraActivityManager +from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager from frigate.comms.base_communicator import Communicator -from frigate.comms.config_updater import ConfigPublisher from frigate.comms.webpush import WebPushClient from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) 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 Event, Previews, Recordings, ReviewSegment @@ -38,7 +47,7 @@ class Dispatcher: def __init__( self, config: FrigateConfig, - config_updater: ConfigPublisher, + config_updater: CameraConfigUpdatePublisher, onvif: OnvifController, ptz_metrics: dict[str, PTZMetrics], communicators: list[Communicator], @@ -49,11 +58,14 @@ class Dispatcher: self.ptz_metrics = ptz_metrics self.comms = communicators self.camera_activity = CameraActivityManager(config, self.publish) - self.model_state = {} - self.embeddings_reindex = {} - + self.audio_activity = AudioActivityManager(config, self.publish) + self.model_state: dict[str, ModelStatusTypesEnum] = {} + self.embeddings_reindex: dict[str, Any] = {} + self.birdseye_layout: dict[str, Any] = {} + self.audio_transcription_state: str = "idle" 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, @@ -68,6 +80,8 @@ class Dispatcher: "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, } self._global_settings_handlers: dict[str, Callable] = { "notifications": self._on_global_notification_command, @@ -80,10 +94,12 @@ class Dispatcher: (comm for comm in communicators if isinstance(comm, WebPushClient)), None ) - def _receive(self, topic: str, payload: str) -> Optional[Any]: + def _receive(self, topic: str, payload: Any) -> Optional[Any]: """Handle receiving of payload from communicators.""" - def handle_camera_command(command_type, camera_name, command, payload): + def handle_camera_command( + command_type: str, camera_name: str, command: str, payload: str + ) -> None: try: if command_type == "set": self._camera_settings_handlers[command](camera_name, payload) @@ -92,13 +108,13 @@ class Dispatcher: except KeyError: logger.error(f"Invalid command type or handler: {command_type}") - def handle_restart(): + def handle_restart() -> None: restart_frigate() - def handle_insert_many_recordings(): + def handle_insert_many_recordings() -> None: Recordings.insert_many(payload).execute() - def handle_request_region_grid(): + def handle_request_region_grid() -> Any: camera = payload grid = get_camera_regions_grid( camera, @@ -107,26 +123,32 @@ class Dispatcher: ) return grid - def handle_insert_preview(): + def handle_insert_preview() -> None: Previews.insert(payload).execute() - def handle_upsert_review_segment(): + def handle_upsert_review_segment() -> None: ReviewSegment.insert(payload).on_conflict( conflict_target=[ReviewSegment.id], update=payload, ).execute() - def handle_clear_ongoing_review_segments(): + 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(): + def handle_update_camera_activity() -> None: self.camera_activity.update_activity(payload) - def handle_update_event_description(): + 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"]) - event.data["description"] = payload["description"] + cast(dict, event.data)["description"] = payload["description"] event.save() self.publish( "tracked_object_update", @@ -140,31 +162,61 @@ class Dispatcher: ), ) - def handle_update_model_state(): + 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(): + def handle_model_state() -> None: self.publish("model_state", json.dumps(self.model_state.copy())) - def handle_update_embeddings_reindex_progress(): + 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(): + def handle_embeddings_reindex_progress() -> None: self.publish( "embeddings_reindex_progress", json.dumps(self.embeddings_reindex.copy()), ) - def handle_on_connect(): + 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 = self.camera_activity.last_camera_activity.copy() + audio_detections = self.audio_activity.current_audio_detections.copy() cameras_with_status = camera_status.keys() for camera in self.config.cameras.keys(): @@ -177,6 +229,9 @@ class Dispatcher: "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) @@ -189,6 +244,12 @@ class Dispatcher: ].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)) @@ -197,8 +258,10 @@ class Dispatcher: "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)) - def handle_notification_test(): + def handle_notification_test() -> None: self.publish("notification_test", "Test notification") # Dictionary mapping topic to handlers @@ -209,13 +272,20 @@ class Dispatcher: 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_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, + "audioTranscriptionState": handle_audio_transcription_state, + "birdseyeLayout": handle_birdseye_layout, "onConnect": handle_on_connect, } @@ -243,11 +313,12 @@ class Dispatcher: logger.error( f"Received invalid {topic.split('/')[-1]} command: {topic}" ) - return + 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.""" @@ -273,8 +344,11 @@ class Dispatcher: f"Turning on motion for {camera_name} due to detection being enabled." ) motion_settings.enabled = True - self.config_updater.publish( - f"config/motion/{camera_name}", motion_settings + 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": @@ -282,7 +356,10 @@ class Dispatcher: logger.info(f"Turning off detection for {camera_name}") detect_settings.enabled = False - self.config_updater.publish(f"config/detect/{camera_name}", detect_settings) + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.detect, camera_name), + detect_settings, + ) self.publish(f"{camera_name}/detect/state", payload, retain=True) def _on_enabled_command(self, camera_name: str, payload: str) -> None: @@ -303,7 +380,10 @@ class Dispatcher: logger.info(f"Turning off camera {camera_name}") camera_settings.enabled = False - self.config_updater.publish(f"config/enabled/{camera_name}", camera_settings) + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, camera_name), + camera_settings.enabled, + ) self.publish(f"{camera_name}/enabled/state", payload, retain=True) def _on_motion_command(self, camera_name: str, payload: str) -> None: @@ -326,7 +406,10 @@ class Dispatcher: logger.info(f"Turning off motion for {camera_name}") motion_settings.enabled = False - self.config_updater.publish(f"config/motion/{camera_name}", motion_settings) + 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( @@ -338,13 +421,16 @@ class Dispatcher: if payload == "ON": if not motion_settings.improve_contrast: logger.info(f"Turning on improve contrast for {camera_name}") - motion_settings.improve_contrast = True # type: ignore[union-attr] + 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 # type: ignore[union-attr] + motion_settings.improve_contrast = False - self.config_updater.publish(f"config/motion/{camera_name}", motion_settings) + 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: @@ -383,8 +469,11 @@ class Dispatcher: motion_settings = self.config.cameras[camera_name].motion logger.info(f"Setting motion contour area for {camera_name}: {payload}") - motion_settings.contour_area = payload # type: ignore[union-attr] - self.config_updater.publish(f"config/motion/{camera_name}", motion_settings) + 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: @@ -397,8 +486,11 @@ class Dispatcher: motion_settings = self.config.cameras[camera_name].motion logger.info(f"Setting motion threshold for {camera_name}: {payload}") - motion_settings.threshold = payload # type: ignore[union-attr] - self.config_updater.publish(f"config/motion/{camera_name}", motion_settings) + 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: @@ -409,9 +501,9 @@ class Dispatcher: notification_settings = self.config.notifications logger.info(f"Setting all notifications: {payload}") - notification_settings.enabled = payload == "ON" # type: ignore[union-attr] - self.config_updater.publish( - "config/notifications", {"_global_notifications": notification_settings} + notification_settings.enabled = payload == "ON" + self.config_updater.publisher.publish( + "config/notifications", notification_settings ) self.publish("notifications/state", payload, retain=True) @@ -434,9 +526,43 @@ class Dispatcher: logger.info(f"Turning off audio detection for {camera_name}") audio_settings.enabled = False - self.config_updater.publish(f"config/audio/{camera_name}", audio_settings) + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.audio, camera_name), + audio_settings, + ) 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 @@ -456,7 +582,10 @@ class Dispatcher: logger.info(f"Turning off recordings for {camera_name}") record_settings.enabled = False - self.config_updater.publish(f"config/record/{camera_name}", record_settings) + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.record, camera_name), + record_settings, + ) self.publish(f"{camera_name}/recordings/state", payload, retain=True) def _on_snapshots_command(self, camera_name: str, payload: str) -> None: @@ -472,25 +601,33 @@ class Dispatcher: 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.publish(f"{camera_name}/snapshots/state", payload, retain=True) - def _on_ptz_command(self, camera_name: str, payload: str) -> None: + def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None: """Callback for ptz topic.""" try: - if "preset" in payload.lower(): + preset: str = ( + payload.decode("utf-8") if isinstance(payload, bytes) else payload + ).lower() + + if "preset" in preset: command = OnvifCommandEnum.preset - param = payload.lower()[payload.index("_") + 1 :] - elif "move_relative" in payload.lower(): + param = preset[preset.index("_") + 1 :] + elif "move_relative" in preset: command = OnvifCommandEnum.move_relative - param = payload.lower()[payload.index("_") + 1 :] + param = preset[preset.index("_") + 1 :] else: - command = OnvifCommandEnum[payload.lower()] + 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 {payload}: {k}") + logger.error(f"Invalid PTZ command {preset}: {k}") def _on_birdseye_command(self, camera_name: str, payload: str) -> None: """Callback for birdseye topic.""" @@ -506,7 +643,10 @@ class Dispatcher: logger.info(f"Turning off birdseye for {camera_name}") birdseye_settings.enabled = False - self.config_updater.publish(f"config/birdseye/{camera_name}", birdseye_settings) + 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: @@ -527,7 +667,10 @@ class Dispatcher: f"Setting birdseye mode for {camera_name} to {birdseye_settings.mode}" ) - self.config_updater.publish(f"config/birdseye/{camera_name}", birdseye_settings) + 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: @@ -559,8 +702,9 @@ class Dispatcher: ): self.web_push_client.suspended_cameras[camera_name] = 0 - self.config_updater.publish( - "config/notifications", {camera_name: notification_settings} + 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) @@ -617,7 +761,10 @@ class Dispatcher: logger.info(f"Turning off alerts for {camera_name}") review_settings.alerts.enabled = False - self.config_updater.publish(f"config/review/{camera_name}", review_settings) + 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: @@ -639,5 +786,58 @@ class Dispatcher: logger.info(f"Turning off detections for {camera_name}") review_settings.detections.enabled = False - self.config_updater.publish(f"config/review/{camera_name}", review_settings) + 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) diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py index 74a87e60f..f7fd9c2bf 100644 --- a/frigate/comms/embeddings_updater.py +++ b/frigate/comms/embeddings_updater.py @@ -1,23 +1,36 @@ """Facilitates communication between processes.""" +import logging from enum import Enum from typing import Any, Callable 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" - embed_description = "embed_description" - embed_thumbnail = "embed_thumbnail" - generate_search = "generate_search" recognize_face = "recognize_face" register_face = "register_face" reprocess_face = "reprocess_face" - reprocess_plate = "reprocess_plate" + # 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: @@ -34,9 +47,16 @@ class EmbeddingsResponder: break try: - (topic, value) = self.socket.recv_json(flags=zmq.NOBLOCK) + raw = self.socket.recv_json(flags=zmq.NOBLOCK) - response = process(topic, value) + 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) @@ -58,7 +78,7 @@ class EmbeddingsRequestor: self.socket = self.context.socket(zmq.REQ) self.socket.connect(SOCKET_REP_REQ) - def send_data(self, topic: str, data: Any) -> str: + def send_data(self, topic: str, data: Any) -> Any: """Sends data and then waits for reply.""" try: self.socket.send_json((topic, data)) diff --git a/frigate/comms/event_metadata_updater.py b/frigate/comms/event_metadata_updater.py index 6305de5a1..897778832 100644 --- a/frigate/comms/event_metadata_updater.py +++ b/frigate/comms/event_metadata_updater.py @@ -15,7 +15,7 @@ class EventMetadataTypeEnum(str, Enum): manual_event_end = "manual_event_end" regenerate_description = "regenerate_description" sub_label = "sub_label" - recognized_license_plate = "recognized_license_plate" + attribute = "attribute" lpr_event_create = "lpr_event_create" save_lpr_snapshot = "save_lpr_snapshot" @@ -28,8 +28,8 @@ class EventMetadataPublisher(Publisher): def __init__(self) -> None: super().__init__() - def publish(self, topic: EventMetadataTypeEnum, payload: Any) -> None: - super().publish(payload, topic.value) + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) class EventMetadataSubscriber(Subscriber): @@ -40,9 +40,10 @@ class EventMetadataSubscriber(Subscriber): def __init__(self, topic: EventMetadataTypeEnum) -> None: super().__init__(topic.value) - def _return_object(self, topic: str, payload: tuple) -> tuple: + def _return_object( + self, topic: str, payload: tuple | None + ) -> tuple[str, Any] | tuple[None, None]: if payload is None: return (None, None) - topic = EventMetadataTypeEnum[topic[len(self.topic_base) :]] return (topic, payload) diff --git a/frigate/comms/events_updater.py b/frigate/comms/events_updater.py index b1d7a6328..cfd958d2c 100644 --- a/frigate/comms/events_updater.py +++ b/frigate/comms/events_updater.py @@ -7,7 +7,9 @@ from frigate.events.types import EventStateEnum, EventTypeEnum from .zmq_proxy import Publisher, Subscriber -class EventUpdatePublisher(Publisher): +class EventUpdatePublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]] +): """Publishes events (objects, audio, manual).""" topic_base = "event/" @@ -16,9 +18,11 @@ class EventUpdatePublisher(Publisher): super().__init__("update") def publish( - self, payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]] + self, + payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]], + sub_topic: str = "", ) -> None: - super().publish(payload) + super().publish(payload, sub_topic) class EventUpdateSubscriber(Subscriber): @@ -30,7 +34,9 @@ class EventUpdateSubscriber(Subscriber): super().__init__("update") -class EventEndPublisher(Publisher): +class EventEndPublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]] +): """Publishes events that have ended.""" topic_base = "event/" @@ -39,9 +45,11 @@ class EventEndPublisher(Publisher): super().__init__("finalized") def publish( - self, payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]] + self, + payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]], + sub_topic: str = "", ) -> None: - super().publish(payload) + super().publish(payload, sub_topic) class EventEndSubscriber(Subscriber): diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py index ee1a78efc..e4aad9107 100644 --- a/frigate/comms/inter_process.py +++ b/frigate/comms/inter_process.py @@ -1,5 +1,6 @@ """Facilitates communication between processes.""" +import logging import multiprocessing as mp import threading from multiprocessing.synchronize import Event as MpEvent @@ -9,6 +10,8 @@ import zmq from frigate.comms.base_communicator import Communicator +logger = logging.getLogger(__name__) + SOCKET_REP_REQ = "ipc:///tmp/cache/comms" @@ -19,7 +22,7 @@ class InterProcessCommunicator(Communicator): self.socket.bind(SOCKET_REP_REQ) self.stop_event: MpEvent = mp.Event() - def publish(self, topic: str, payload: str, retain: bool) -> None: + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: """There is no communication back to the processes.""" pass @@ -37,9 +40,16 @@ class InterProcessCommunicator(Communicator): break try: - (topic, value) = self.socket.recv_json(flags=zmq.NOBLOCK) + raw = self.socket.recv_json(flags=zmq.NOBLOCK) - response = self._dispatcher(topic, value) + 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) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index e487b30ee..68ae698d9 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -11,7 +11,7 @@ from frigate.config import FrigateConfig logger = logging.getLogger(__name__) -class MqttClient(Communicator): # type: ignore[misc] +class MqttClient(Communicator): """Frigate wrapper for mqtt client.""" def __init__(self, config: FrigateConfig) -> None: @@ -75,7 +75,7 @@ class MqttClient(Communicator): # type: ignore[misc] ) self.publish( f"{camera_name}/improve_contrast/state", - "ON" if camera.motion.improve_contrast else "OFF", # type: ignore[union-attr] + "ON" if camera.motion.improve_contrast else "OFF", retain=True, ) self.publish( @@ -85,12 +85,12 @@ class MqttClient(Communicator): # type: ignore[misc] ) self.publish( f"{camera_name}/motion_threshold/state", - camera.motion.threshold, # type: ignore[union-attr] + camera.motion.threshold, retain=True, ) self.publish( f"{camera_name}/motion_contour_area/state", - camera.motion.contour_area, # type: ignore[union-attr] + camera.motion.contour_area, retain=True, ) self.publish( @@ -122,6 +122,16 @@ class MqttClient(Communicator): # type: ignore[misc] "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, + ) if self.config.notifications.enabled_in_config: self.publish( @@ -145,7 +155,7 @@ class MqttClient(Communicator): # type: ignore[misc] client: mqtt.Client, userdata: Any, flags: Any, - reason_code: mqtt.ReasonCode, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] properties: Any, ) -> None: """Mqtt connection callback.""" @@ -177,7 +187,7 @@ class MqttClient(Communicator): # type: ignore[misc] client: mqtt.Client, userdata: Any, flags: Any, - reason_code: mqtt.ReasonCode, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] properties: Any, ) -> None: """Mqtt disconnection callback.""" @@ -215,6 +225,8 @@ class MqttClient(Communicator): # type: ignore[misc] "birdseye_mode", "review_alerts", "review_detections", + "object_descriptions", + "review_descriptions", ] for name in self.config.cameras.keys(): diff --git a/frigate/comms/object_detector_signaler.py b/frigate/comms/object_detector_signaler.py new file mode 100644 index 000000000..e8871db1a --- /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 index 862ec1041..249c2f607 100644 --- a/frigate/comms/recordings_updater.py +++ b/frigate/comms/recordings_updater.py @@ -2,6 +2,7 @@ import logging from enum import Enum +from typing import Any from .zmq_proxy import Publisher, Subscriber @@ -10,20 +11,22 @@ logger = logging.getLogger(__name__) class RecordingsDataTypeEnum(str, Enum): all = "" - recordings_available_through = "recordings_available_through" + 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): +class RecordingsDataPublisher(Publisher[Any]): """Publishes latest recording data.""" topic_base = "recordings/" - def __init__(self, topic: RecordingsDataTypeEnum) -> None: - topic = topic.value - super().__init__(topic) + def __init__(self) -> None: + super().__init__() - def publish(self, payload: tuple[str, float]) -> None: - super().publish(payload) + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) class RecordingsDataSubscriber(Subscriber): @@ -32,5 +35,12 @@ class RecordingsDataSubscriber(Subscriber): topic_base = "recordings/" def __init__(self, topic: RecordingsDataTypeEnum) -> None: - topic = topic.value - super().__init__(topic) + 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 000000000..2b3a5b3aa --- /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/webpush.py b/frigate/comms/webpush.py index c5986d45c..62cc12c9a 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -17,7 +17,11 @@ 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.const import CONFIG_DIR +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__) @@ -35,7 +39,7 @@ class PushNotification: ttl: int = 0 -class WebPushClient(Communicator): # type: ignore[misc] +class WebPushClient(Communicator): """Frigate wrapper for webpush client.""" def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: @@ -46,10 +50,12 @@ class WebPushClient(Communicator): # type: ignore[misc] self.web_pushers: dict[str, list[WebPusher]] = {} self.expired_subs: dict[str, list[str]] = {} self.suspended_cameras: dict[str, int] = { - c.name: 0 for c in self.config.cameras.values() + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() } self.last_camera_notification_time: dict[str, float] = { - c.name: 0 for c in self.config.cameras.values() + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() } self.last_notification_time: float = 0 self.notification_queue: queue.Queue[PushNotification] = queue.Queue() @@ -64,7 +70,7 @@ class WebPushClient(Communicator): # type: ignore[misc] # 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[User] = ( + users: list[dict[str, Any]] = ( User.select(User.username, User.notification_tokens).dicts().iterator() ) for user in users: @@ -73,7 +79,12 @@ class WebPushClient(Communicator): # type: ignore[misc] self.web_pushers[user["username"]].append(WebPusher(sub)) # notification config updater - self.config_subscriber = ConfigSubscriber("config/notifications") + self.global_config_subscriber = ConfigSubscriber( + "config/notifications", exact=True + ) + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, self.config.cameras, [CameraConfigUpdateEnum.notifications] + ) def subscribe(self, receiver: Callable) -> None: """Wrapper for allowing dispatcher to subscribe.""" @@ -154,15 +165,19 @@ class WebPushClient(Communicator): # type: ignore[misc] def publish(self, topic: str, payload: Any, retain: bool = False) -> None: """Wrapper for publishing when client is in valid state.""" # check for updated notification config - _, updated_notification_config = self.config_subscriber.check_for_update() + _, updated_notification_config = ( + self.global_config_subscriber.check_for_update() + ) if updated_notification_config: - for key, value in updated_notification_config.items(): - if key == "_global_notifications": - self.config.notifications = value + self.config.notifications = updated_notification_config - elif key in self.config.cameras: - self.config.cameras[key].notifications = value + 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) @@ -173,6 +188,28 @@ class WebPushClient(Communicator): # type: ignore[misc] 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 == "notification_test": if not self.config.notifications.enabled and not any( cam.notifications.enabled for cam in self.config.cameras.values() @@ -254,6 +291,23 @@ class WebPushClient(Communicator): # type: ignore[misc] except Exception as e: logger.error(f"Error processing notification: {str(e)}") + 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 @@ -280,26 +334,12 @@ class WebPushClient(Communicator): # type: ignore[misc] 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() - # Check global cooldown period - if ( - current_time - self.last_notification_time - < self.config.notifications.cooldown - ): - logger.debug( - f"Skipping notification for {camera} - in global cooldown period" - ) - return - - # Check camera-specific cooldown period - if ( - current_time - self.last_camera_notification_time[camera] - < self.config.cameras[camera].notifications.cooldown - ): - logger.debug( - f"Skipping notification for {camera} - in camera-specific cooldown period" - ) + if self._within_cooldown(camera): return self.check_registrations() @@ -331,13 +371,49 @@ class WebPushClient(Communicator): # type: ignore[misc] sorted_objects.update(payload["after"]["data"]["sub_labels"]) - title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}" - message = f"Detected on {titlecase(camera.replace('_', ' '))}" - image = f"{payload['after']['thumb_path'].replace('/media/frigate', '')}" + 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 + + message = payload["after"]["data"]["metadata"]["shortSummary"] + 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 state == "end" else f"/#{camera}" - ttl = 3600 if state == "end" else 0 + 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}") @@ -354,6 +430,53 @@ class WebPushClient(Communicator): # type: ignore[misc] 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: + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + ttl=ttl, + ) + + 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 index 1eed290f7..6cfe4ecc0 100644 --- a/frigate/comms/ws.py +++ b/frigate/comms/ws.py @@ -4,7 +4,7 @@ import errno import json import logging import threading -from typing import Callable +from typing import Any, Callable from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import ( @@ -21,8 +21,8 @@ from frigate.config import FrigateConfig logger = logging.getLogger(__name__) -class WebSocket(WebSocket_): - def unhandled_error(self, error): +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 @@ -33,12 +33,12 @@ class WebSocket(WebSocket_): logging.getLogger("ws4py").exception("Failed to receive data") -class WebSocketClient(Communicator): # type: ignore[misc] +class WebSocketClient(Communicator): """Frigate wrapper for ws client.""" def __init__(self, config: FrigateConfig) -> None: self.config = config - self.websocket_server = None + self.websocket_server: WSGIServer | None = None def subscribe(self, receiver: Callable) -> None: self._dispatcher = receiver @@ -47,10 +47,10 @@ class WebSocketClient(Communicator): # type: ignore[misc] def start(self) -> None: """Start the websocket client.""" - class _WebSocketHandler(WebSocket): # type: ignore[misc] + class _WebSocketHandler(WebSocket): receiver = self._dispatcher - def received_message(self, message: WebSocket.received_message) -> None: + 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 = { @@ -86,7 +86,7 @@ class WebSocketClient(Communicator): # type: ignore[misc] ) self.websocket_thread.start() - def publish(self, topic: str, payload: str, _: bool) -> None: + def publish(self, topic: str, payload: Any, _: bool = False) -> None: try: ws_message = json.dumps( { @@ -109,9 +109,11 @@ class WebSocketClient(Communicator): # type: ignore[misc] pass def stop(self) -> None: - self.websocket_server.manager.close_all() - self.websocket_server.manager.stop() - self.websocket_server.manager.join() - self.websocket_server.shutdown() + 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 index d26da3312..29329ec59 100644 --- a/frigate/comms/zmq_proxy.py +++ b/frigate/comms/zmq_proxy.py @@ -2,7 +2,7 @@ import json import threading -from typing import Any, Optional +from typing import Generic, TypeVar import zmq @@ -47,7 +47,10 @@ class ZmqProxy: self.runner.join() -class Publisher: +T = TypeVar("T") + + +class Publisher(Generic[T]): """Publishes messages.""" topic_base: str = "" @@ -58,7 +61,7 @@ class Publisher: self.socket = self.context.socket(zmq.PUB) self.socket.connect(SOCKET_PUB) - def publish(self, payload: Any, sub_topic: str = "") -> None: + def publish(self, payload: T, sub_topic: str = "") -> None: """Publish message.""" self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") @@ -67,7 +70,7 @@ class Publisher: self.context.destroy() -class Subscriber: +class Subscriber(Generic[T]): """Receives messages.""" topic_base: str = "" @@ -79,9 +82,7 @@ class Subscriber: self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) self.socket.connect(SOCKET_SUB) - def check_for_update( - self, timeout: float = FAST_QUEUE_TIMEOUT - ) -> Optional[tuple[str, Any]]: + 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) @@ -98,5 +99,5 @@ class Subscriber: self.socket.close() self.context.destroy() - def _return_object(self, topic: str, payload: Any) -> Any: + def _return_object(self, topic: str, payload: T | None) -> T | None: return payload diff --git a/frigate/config/auth.py b/frigate/config/auth.py index a202fb1af..6935350a0 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -1,6 +1,6 @@ -from typing import Optional +from typing import Dict, List, Optional -from pydantic import Field +from pydantic import Field, field_validator, model_validator from .base import FrigateBaseModel @@ -20,7 +20,7 @@ class AuthConfig(FrigateBaseModel): default=86400, title="Session length for jwt session tokens", ge=60 ) refresh_time: int = Field( - default=43200, + default=1800, title="Refresh the session if it is going to expire in this many seconds", ge=30, ) @@ -34,3 +34,48 @@ class AuthConfig(FrigateBaseModel): ) # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 hash_iterations: int = Field(default=600000, title="Password hash iterations") + roles: Dict[str, List[str]] = Field( + default_factory=dict, + title="Role to camera mappings. Empty list grants access to all cameras.", + ) + admin_first_time_login: Optional[bool] = Field( + default=False, + title="Internal field to expose first-time admin login flag to the UI", + 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 index 068a68acd..1e369e293 100644 --- a/frigate/config/base.py +++ b/frigate/config/base.py @@ -1,5 +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/birdseye.py b/frigate/config/camera/birdseye.py index b7e8a7117..1e6f0f335 100644 --- a/frigate/config/camera/birdseye.py +++ b/frigate/config/camera/birdseye.py @@ -55,6 +55,12 @@ class BirdseyeConfig(FrigateBaseModel): layout: BirdseyeLayoutConfig = Field( default_factory=BirdseyeLayoutConfig, title="Birdseye Layout Config" ) + idle_heartbeat_fps: float = Field( + default=0.0, + ge=0.0, + le=10.0, + title="Idle heartbeat FPS (0 disables, max 10)", + ) # uses BaseModel because some global attributes are not available at the camera level diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index 3b24dabac..0f2b1c8be 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -2,7 +2,7 @@ import os from enum import Enum from typing import Optional -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, model_validator from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME from frigate.ffmpeg_presets import ( @@ -19,14 +19,15 @@ from frigate.util.builtin import ( 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 .genai import GenAICameraConfig from .live import CameraLiveConfig from .motion import MotionConfig from .mqtt import CameraMqttConfig @@ -50,12 +51,28 @@ class CameraTypeEnum(str, Enum): class CameraConfig(FrigateBaseModel): name: Optional[str] = Field(None, title="Camera name.", pattern=REGEX_CAMERA_NAME) + + friendly_name: Optional[str] = Field( + None, title="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="Enable camera.") # Options with global fallback audio: AudioConfig = Field( default_factory=AudioConfig, title="Audio events configuration." ) + audio_transcription: CameraAudioTranscriptionConfig = Field( + default_factory=CameraAudioTranscriptionConfig, + title="Audio transcription config.", + ) birdseye: BirdseyeCameraConfig = Field( default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration." ) @@ -66,18 +83,13 @@ class CameraConfig(FrigateBaseModel): default_factory=CameraFaceRecognitionConfig, title="Face recognition config." ) ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") - genai: GenAICameraConfig = Field( - default_factory=GenAICameraConfig, title="Generative AI configuration." - ) live: CameraLiveConfig = Field( default_factory=CameraLiveConfig, title="Live playback settings." ) lpr: CameraLicensePlateRecognitionConfig = Field( default_factory=CameraLicensePlateRecognitionConfig, title="LPR config." ) - motion: Optional[MotionConfig] = Field( - None, title="Motion detection configuration." - ) + motion: MotionConfig = Field(None, title="Motion detection configuration.") objects: ObjectConfig = Field( default_factory=ObjectConfig, title="Object configuration." ) @@ -87,6 +99,10 @@ class CameraConfig(FrigateBaseModel): review: ReviewConfig = Field( default_factory=ReviewConfig, title="Review configuration." ) + semantic_search: CameraSemanticSearchConfig = Field( + default_factory=CameraSemanticSearchConfig, + title="Semantic search configuration.", + ) snapshots: SnapshotsConfig = Field( default_factory=SnapshotsConfig, title="Snapshot configuration." ) @@ -161,6 +177,12 @@ class CameraConfig(FrigateBaseModel): 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 @@ -219,6 +241,7 @@ class CameraConfig(FrigateBaseModel): self.detect.fps, self.detect.width, self.detect.height, + self.ffmpeg.gpu, ) or ffmpeg_input.hwaccel_args or parse_preset_hardware_acceleration_decode( @@ -226,6 +249,7 @@ class CameraConfig(FrigateBaseModel): self.detect.fps, self.detect.width, self.detect.height, + self.ffmpeg.gpu, ) or camera_arg or [] diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py index 99e02c2c8..1926f3254 100644 --- a/frigate/config/camera/detect.py +++ b/frigate/config/camera/detect.py @@ -29,6 +29,10 @@ class StationaryConfig(FrigateBaseModel): default_factory=StationaryMaxFramesConfig, title="Max frames for stationary objects.", ) + classifier: bool = Field( + default=True, + title="Enable visual classifier for determing if objects with jittery bounding boxes are stationary.", + ) class DetectConfig(FrigateBaseModel): diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index dd65fdcd4..2c1e4cdca 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -67,6 +67,7 @@ class FfmpegConfig(FrigateBaseModel): default=False, title="Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players.", ) + gpu: int = Field(default=0, title="GPU index to use for hardware acceleration.") @property def ffmpeg_path(self) -> str: diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py index 6ef93682b..a4d9199af 100644 --- a/frigate/config/camera/genai.py +++ b/frigate/config/camera/genai.py @@ -1,12 +1,12 @@ from enum import Enum -from typing import Optional, Union +from typing import Any, Optional -from pydantic import BaseModel, Field, field_validator +from pydantic import Field from ..base import FrigateBaseModel from ..env import EnvString -__all__ = ["GenAIConfig", "GenAICameraConfig", "GenAIProviderEnum"] +__all__ = ["GenAIConfig", "GenAIProviderEnum"] class GenAIProviderEnum(str, Enum): @@ -16,70 +16,16 @@ class GenAIProviderEnum(str, Enum): ollama = "ollama" -class GenAISendTriggersConfig(BaseModel): - tracked_object_end: bool = Field( - default=True, title="Send once the object is no longer tracked." - ) - after_significant_updates: Optional[int] = Field( - default=None, - title="Send an early request to generative AI when X frames accumulated.", - ge=1, - ) - - -# uses BaseModel because some global attributes are not available at the camera level -class GenAICameraConfig(BaseModel): - enabled: bool = Field(default=False, title="Enable GenAI for camera.") - use_snapshot: bool = Field( - default=False, title="Use snapshots for generating descriptions." - ) - 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="Default caption prompt.", - ) - object_prompts: dict[str, str] = Field( - default_factory=dict, title="Object specific prompts." - ) - - objects: Union[str, list[str]] = Field( - default_factory=list, - title="List of objects to run generative AI for.", - ) - required_zones: Union[str, list[str]] = Field( - default_factory=list, - title="List of required zones to be entered in order to run generative AI.", - ) - debug_save_thumbnails: bool = Field( - default=False, - title="Save thumbnails sent to generative AI for debugging purposes.", - ) - send_triggers: GenAISendTriggersConfig = Field( - default_factory=GenAISendTriggersConfig, - title="What triggers to use to send frames to generative AI for a tracked object.", - ) - - @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 GenAIConfig(FrigateBaseModel): - enabled: bool = Field(default=False, title="Enable GenAI.") - 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="Default caption prompt.", - ) - object_prompts: dict[str, str] = Field( - default_factory=dict, title="Object specific prompts." - ) + """Primary GenAI Config to define GenAI Provider.""" api_key: Optional[EnvString] = Field(default=None, title="Provider API key.") base_url: Optional[str] = Field(default=None, title="Provider base url.") model: str = Field(default="gpt-4o", title="GenAI model.") - provider: GenAIProviderEnum = Field( - default=GenAIProviderEnum.openai, title="GenAI provider." + provider: GenAIProviderEnum | None = Field(default=None, title="GenAI provider.") + provider_options: dict[str, Any] = Field( + default={}, title="GenAI Provider extra options." + ) + runtime_options: dict[str, Any] = Field( + default={}, title="Options to pass during inference calls." ) diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py index b0d7cebf9..ce1ac8223 100644 --- a/frigate/config/camera/notification.py +++ b/frigate/config/camera/notification.py @@ -10,7 +10,7 @@ __all__ = ["NotificationConfig"] class NotificationConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable notifications") email: Optional[str] = Field(default=None, title="Email required for push.") - cooldown: Optional[int] = Field( + cooldown: int = Field( default=0, ge=0, title="Cooldown period for notifications (time in seconds)." ) enabled_in_config: Optional[bool] = Field( diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py index 0d559b6ce..7b6317dd0 100644 --- a/frigate/config/camera/objects.py +++ b/frigate/config/camera/objects.py @@ -1,10 +1,10 @@ from typing import Any, Optional, Union -from pydantic import Field, PrivateAttr, field_serializer +from pydantic import Field, PrivateAttr, field_serializer, field_validator from ..base import FrigateBaseModel -__all__ = ["ObjectConfig", "FilterConfig"] +__all__ = ["ObjectConfig", "GenAIObjectConfig", "FilterConfig"] DEFAULT_TRACKED_OBJECTS = ["person"] @@ -49,12 +49,69 @@ class FilterConfig(FrigateBaseModel): return None +class GenAIObjectTriggerConfig(FrigateBaseModel): + tracked_object_end: bool = Field( + default=True, title="Send once the object is no longer tracked." + ) + after_significant_updates: Optional[int] = Field( + default=None, + title="Send an early request to generative AI when X frames accumulated.", + ge=1, + ) + + +class GenAIObjectConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable GenAI for camera.") + use_snapshot: bool = Field( + default=False, title="Use snapshots for generating descriptions." + ) + 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="Default caption prompt.", + ) + object_prompts: dict[str, str] = Field( + default_factory=dict, title="Object specific prompts." + ) + + objects: Union[str, list[str]] = Field( + default_factory=list, + title="List of objects to run generative AI for.", + ) + required_zones: Union[str, list[str]] = Field( + default_factory=list, + title="List of required zones to be entered in order to run generative AI.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails sent to generative AI for debugging purposes.", + ) + send_triggers: GenAIObjectTriggerConfig = Field( + default_factory=GenAIObjectTriggerConfig, + title="What triggers to use to send frames to generative AI for a tracked object.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of generative AI." + ) + + @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.") filters: dict[str, FilterConfig] = Field( default_factory=dict, title="Object filters." ) mask: Union[str, list[str]] = Field(default="", title="Object mask.") + genai: GenAIObjectConfig = Field( + default_factory=GenAIObjectConfig, + title="Config for using genai to analyze objects.", + ) _all_objects: list[str] = PrivateAttr() @property diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py index 52d11e2a5..09a7a84d5 100644 --- a/frigate/config/camera/record.py +++ b/frigate/config/camera/record.py @@ -22,27 +22,31 @@ __all__ = [ DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" +class RecordRetainConfig(FrigateBaseModel): + days: float = Field(default=0, ge=0, title="Default retention period.") + + class RetainModeEnum(str, Enum): all = "all" motion = "motion" active_objects = "active_objects" -class RecordRetainConfig(FrigateBaseModel): - days: float = Field(default=0, title="Default retention period.") - mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.") - - class ReviewRetainConfig(FrigateBaseModel): - days: float = Field(default=10, title="Default retention period.") + days: float = Field(default=10, ge=0, title="Default retention period.") mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.") class EventsConfig(FrigateBaseModel): pre_capture: int = Field( - default=5, title="Seconds to retain before event starts.", le=MAX_PRE_CAPTURE + default=5, + title="Seconds to retain before event starts.", + le=MAX_PRE_CAPTURE, + ge=0, + ) + post_capture: int = Field( + default=5, ge=0, title="Seconds to retain after event ends." ) - post_capture: int = Field(default=5, title="Seconds to retain after event ends.") retain: ReviewRetainConfig = Field( default_factory=ReviewRetainConfig, title="Event retention settings." ) @@ -77,8 +81,12 @@ class RecordConfig(FrigateBaseModel): default=60, title="Number of minutes to wait between cleanup runs.", ) - retain: RecordRetainConfig = Field( - default_factory=RecordRetainConfig, title="Record retention settings." + continuous: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, + title="Continuous recording retention settings.", + ) + motion: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, title="Motion recording retention settings." ) detections: EventsConfig = Field( default_factory=EventsConfig, title="Detection specific retention settings." diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py index d8d26edb9..67ba3b60c 100644 --- a/frigate/config/camera/review.py +++ b/frigate/config/camera/review.py @@ -1,10 +1,18 @@ +from enum import Enum from typing import Optional, Union from pydantic import Field, field_validator from ..base import FrigateBaseModel -__all__ = ["ReviewConfig", "DetectionsConfig", "AlertsConfig"] +__all__ = ["ReviewConfig", "DetectionsConfig", "AlertsConfig", "ImageSourceEnum"] + + +class ImageSourceEnum(str, Enum): + """Image source options for GenAI Review.""" + + preview = "preview" + recordings = "recordings" DEFAULT_ALERT_OBJECTS = ["person", "car"] @@ -26,6 +34,10 @@ class AlertsConfig(FrigateBaseModel): enabled_in_config: Optional[bool] = Field( default=None, title="Keep track of original state of alerts." ) + cutoff_time: int = Field( + default=40, + title="Time to cutoff alerts after no alert-causing activity has occurred.", + ) @field_validator("required_zones", mode="before") @classmethod @@ -48,6 +60,10 @@ class DetectionsConfig(FrigateBaseModel): default_factory=list, title="List of required zones to be entered in order to save the event as a detection.", ) + cutoff_time: int = Field( + default=30, + title="Time to cutoff detection after no detection-causing activity has occurred.", + ) enabled_in_config: Optional[bool] = Field( default=None, title="Keep track of original state of detections." @@ -62,6 +78,70 @@ class DetectionsConfig(FrigateBaseModel): return v +class GenAIReviewConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable GenAI descriptions for review items.", + ) + alerts: bool = Field(default=True, title="Enable GenAI for alerts.") + detections: bool = Field(default=False, title="Enable GenAI for detections.") + image_source: ImageSourceEnum = Field( + default=ImageSourceEnum.preview, + title="Image source for review descriptions.", + ) + additional_concerns: list[str] = Field( + default=[], + title="Additional concerns that GenAI should make note of on this camera.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails sent to generative AI for debugging purposes.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of generative AI." + ) + preferred_language: str | None = Field( + title="Preferred language for GenAI Response", + 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 +- 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.""", + title="Custom activity context prompt defining normal and suspicious activity patterns for this property.", + ) + + class ReviewConfig(FrigateBaseModel): """Configure reviews""" @@ -71,3 +151,6 @@ class ReviewConfig(FrigateBaseModel): detections: DetectionsConfig = Field( default_factory=DetectionsConfig, title="Review detections config." ) + genai: GenAIReviewConfig = Field( + default_factory=GenAIReviewConfig, title="Review description genai config." + ) diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py new file mode 100644 index 000000000..125094f10 --- /dev/null +++ b/frigate/config/camera/updater.py @@ -0,0 +1,147 @@ +"""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" + motion = "motion" # includes motion and motion masks + notifications = "notifications" + objects = "objects" + object_genai = "object_genai" + record = "record" + remove = "remove" # for removing a camera + review = "review" + review_genai = "review_genai" + semantic_search = "semantic_search" # for semantic search triggers + snapshots = "snapshots" + 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" + + if 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: + self.config.cameras[camera] = updated_config + self.camera_configs[camera] = updated_config + return + elif update_type == CameraConfigUpdateEnum.remove: + self.config.cameras.pop(camera) + self.camera_configs.pop(camera) + return + + config = self.camera_configs.get(camera) + + if not config: + return + + if update_type == CameraConfigUpdateEnum.audio: + config.audio = updated_config + 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.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: + config.record = updated_config + 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.snapshots: + config.snapshots = 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 index 3e69240d5..7df1a1f25 100644 --- a/frigate/config/camera/zone.py +++ b/frigate/config/camera/zone.py @@ -13,6 +13,9 @@ logger = logging.getLogger(__name__) class ZoneConfig(BaseModel): + friendly_name: Optional[str] = Field( + None, title="Zone friendly name used in the Frigate UI." + ) filters: dict[str, FilterConfig] = Field( default_factory=dict, title="Zone filters." ) @@ -53,6 +56,12 @@ class ZoneConfig(BaseModel): 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): diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 06e69a774..fb8e3de29 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -8,8 +8,10 @@ from .base import FrigateBaseModel __all__ = [ "CameraFaceRecognitionConfig", "CameraLicensePlateRecognitionConfig", + "CameraAudioTranscriptionConfig", "FaceRecognitionConfig", "SemanticSearchConfig", + "CameraSemanticSearchConfig", "LicensePlateRecognitionConfig", ] @@ -19,11 +21,45 @@ class SemanticSearchModelEnum(str, Enum): jinav2 = "jinav2" -class LPRDeviceEnum(str, Enum): +class EnrichmentsDeviceEnum(str, Enum): GPU = "GPU" CPU = "CPU" +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.") + language: str = Field( + default="en", + title="Language abbreviation to use for audio event transcription/translation.", + ) + device: Optional[EnrichmentsDeviceEnum] = Field( + default=EnrichmentsDeviceEnum.CPU, + title="The device used for audio transcription.", + ) + model_size: str = Field( + default="small", title="The size of the embeddings model used." + ) + live_enabled: Optional[bool] = Field( + default=False, title="Enable live transcriptions." + ) + + class BirdClassificationConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable bird classification.") threshold: float = Field( @@ -34,10 +70,57 @@ class BirdClassificationConfig(FrigateBaseModel): ) +class CustomClassificationStateCameraConfig(FrigateBaseModel): + crop: list[float, float, float, float] = Field( + title="Crop of image frame on this camera to run classification on." + ) + + +class CustomClassificationStateConfig(FrigateBaseModel): + cameras: Dict[str, CustomClassificationStateCameraConfig] = Field( + title="Cameras to run classification on." + ) + motion: bool = Field( + default=False, + title="If classification should be run when motion is detected in the crop.", + ) + interval: int | None = Field( + default=None, + title="Interval to run classification on in seconds.", + gt=0, + ) + + +class CustomClassificationObjectConfig(FrigateBaseModel): + objects: list[str] = Field(title="Object types to classify.") + classification_type: ObjectClassificationType = Field( + default=ObjectClassificationType.sub_label, + title="Type of classification that is applied.", + ) + + +class CustomClassificationConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable running the model.") + name: str | None = Field(default=None, title="Name of classification model.") + threshold: float = Field( + default=0.8, title="Classification score threshold to change the state." + ) + save_attempts: int | None = Field( + default=None, + title="Number of classification attempts to save in the recent classifications tab. If not specified, defaults to 200 for object classification and 100 for state classification.", + 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." ) + custom: Dict[str, CustomClassificationConfig] = Field( + default={}, title="Custom Classification Model Configs." + ) class SemanticSearchConfig(FrigateBaseModel): @@ -52,6 +135,40 @@ class SemanticSearchConfig(FrigateBaseModel): model_size: str = Field( default="small", title="The size of the embeddings model used." ) + device: Optional[str] = Field( + default=None, + title="The device key to use for semantic search.", + 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: Optional[str] = Field( + None, title="Trigger friendly name used in the Frigate UI." + ) + enabled: bool = Field(default=True, title="Enable this trigger") + type: TriggerType = Field(default=TriggerType.DESCRIPTION, title="Type of trigger") + data: str = Field(title="Trigger content (text phrase or image ID)") + threshold: float = Field( + title="Confidence score required to run the trigger", + default=0.8, + gt=0.0, + le=1.0, + ) + actions: List[TriggerAction] = Field( + default=[], title="Actions to perform when trigger is matched" + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraSemanticSearchConfig(FrigateBaseModel): + triggers: Dict[str, TriggerConfig] = Field( + default={}, + title="Trigger actions on tracked objects that match existing thumbnails or descriptions", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) class FaceRecognitionConfig(FrigateBaseModel): @@ -87,11 +204,18 @@ class FaceRecognitionConfig(FrigateBaseModel): title="Min face recognitions for the sub label to be applied to the person object.", ) save_attempts: int = Field( - default=100, ge=0, title="Number of face attempts to save in the train tab." + default=200, + ge=0, + title="Number of face attempts to save in the recent recognitions tab.", ) blur_confidence_filter: bool = Field( default=True, title="Apply blur quality filter to face confidence." ) + device: Optional[str] = Field( + default=None, + title="The device key to use for face recognition.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) class CameraFaceRecognitionConfig(FrigateBaseModel): @@ -103,12 +227,15 @@ class CameraFaceRecognitionConfig(FrigateBaseModel): model_config = ConfigDict(extra="forbid", protected_namespaces=()) +class ReplaceRule(FrigateBaseModel): + pattern: str = Field(..., title="Regex pattern to match.") + replacement: str = Field( + ..., title="Replacement string (supports backrefs like '\\1')." + ) + + class LicensePlateRecognitionConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable license plate recognition.") - device: Optional[LPRDeviceEnum] = Field( - default=LPRDeviceEnum.CPU, - title="The device used for license plate recognition.", - ) model_size: str = Field( default="small", title="The size of the embeddings model used." ) @@ -154,6 +281,15 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): default=False, title="Save plates captured for LPR for debugging purposes.", ) + device: Optional[str] = Field( + default=None, + title="The device key to use for LPR.", + 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="List of regex replacement rules for normalizing detected plates. Each rule has 'pattern' and 'replacement'.", + ) class CameraLicensePlateRecognitionConfig(FrigateBaseModel): @@ -175,3 +311,15 @@ class CameraLicensePlateRecognitionConfig(FrigateBaseModel): ) model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraAudioTranscriptionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable audio transcription.") + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of audio transcription." + ) + live_enabled: Optional[bool] = Field( + default=False, title="Enable live transcriptions." + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) diff --git a/frigate/config/config.py b/frigate/config/config.py index 6ec048acd..a26d4c50e 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -28,6 +28,7 @@ from frigate.util.builtin import ( get_ffmpeg_arg_list, ) from frigate.util.config import ( + CURRENT_CONFIG_VERSION, StreamInfoRetriever, convert_area_to_pixels, find_config_file, @@ -48,12 +49,13 @@ from .camera.genai import GenAIConfig from .camera.motion import MotionConfig from .camera.notification import NotificationConfig from .camera.objects import FilterConfig, ObjectConfig -from .camera.record import RecordConfig, RetainModeEnum +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, @@ -63,6 +65,7 @@ from .database import DatabaseConfig from .env import EnvVars from .logger import LoggerConfig from .mqtt import MqttConfig +from .network import NetworkingConfig from .proxy import ProxyConfig from .telemetry import TelemetryConfig from .tls import TlsConfig @@ -74,22 +77,12 @@ logger = logging.getLogger(__name__) yaml = YAML() -DEFAULT_CONFIG = """ +DEFAULT_CONFIG = f""" mqtt: enabled: False -cameras: - name_of_your_camera: # <------ Name the camera - enabled: True - ffmpeg: - inputs: - - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection - roles: - - detect - detect: - enabled: False # <---- disable detection until you have a working camera feed - width: 1280 - height: 720 +cameras: {{}} # No cameras defined, UI wizard should be used +version: {CURRENT_CONFIG_VERSION} """ DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} @@ -203,33 +196,6 @@ def verify_valid_live_stream_names( ) -def verify_recording_retention(camera_config: CameraConfig) -> None: - """Verify that recording retention modes are ranked correctly.""" - rank_map = { - RetainModeEnum.all: 0, - RetainModeEnum.motion: 1, - RetainModeEnum.active_objects: 2, - } - - if ( - camera_config.record.retain.days != 0 - and rank_map[camera_config.record.retain.mode] - > rank_map[camera_config.record.alerts.retain.mode] - ): - logger.warning( - f"{camera_config.name}: Recording retention is configured for {camera_config.record.retain.mode} and alert retention is configured for {camera_config.record.alerts.retain.mode}. The more restrictive retention policy will be applied." - ) - - if ( - camera_config.record.retain.days != 0 - and rank_map[camera_config.record.retain.mode] - > rank_map[camera_config.record.detections.retain.mode] - ): - logger.warning( - f"{camera_config.name}: Recording retention is configured for {camera_config.record.retain.mode} and detection retention is configured for {camera_config.record.detections.retain.mode}. The more restrictive retention policy will be applied." - ) - - def verify_recording_segments_setup_with_reasonable_time( camera_config: CameraConfig, ) -> None: @@ -334,6 +300,9 @@ def verify_lpr_and_face( class FrigateConfig(FrigateBaseModel): version: Optional[str] = Field(default=None, title="Current config version.") + safe_mode: bool = Field( + default=False, title="If Frigate should be started in safe mode." + ) # Fields that install global state should be defined first, so that their validators run first. environment_vars: EnvVars = Field( @@ -357,6 +326,9 @@ class FrigateConfig(FrigateBaseModel): notifications: NotificationConfig = Field( default_factory=NotificationConfig, title="Global notification configuration." ) + networking: NetworkingConfig = Field( + default_factory=NetworkingConfig, title="Networking configuration" + ) proxy: ProxyConfig = Field( default_factory=ProxyConfig, title="Proxy configuration." ) @@ -375,6 +347,11 @@ class FrigateConfig(FrigateBaseModel): default_factory=ModelConfig, title="Detection model configuration." ) + # GenAI config + genai: GenAIConfig = Field( + default_factory=GenAIConfig, title="Generative AI configuration." + ) + # Camera config cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.") audio: AudioConfig = Field( @@ -389,9 +366,6 @@ class FrigateConfig(FrigateBaseModel): ffmpeg: FfmpegConfig = Field( default_factory=FfmpegConfig, title="Global FFmpeg configuration." ) - genai: GenAIConfig = Field( - default_factory=GenAIConfig, title="Generative AI configuration." - ) live: CameraLiveConfig = Field( default_factory=CameraLiveConfig, title="Live playback settings." ) @@ -416,6 +390,9 @@ class FrigateConfig(FrigateBaseModel): ) # Classification Config + audio_transcription: AudioTranscriptionConfig = Field( + default_factory=AudioTranscriptionConfig, title="Audio transcription config." + ) classification: ClassificationConfig = Field( default_factory=ClassificationConfig, title="Object classification config." ) @@ -469,6 +446,7 @@ class FrigateConfig(FrigateBaseModel): global_config = self.model_dump( include={ "audio": ..., + "audio_transcription": ..., "birdseye": ..., "face_recognition": ..., "lpr": ..., @@ -477,7 +455,6 @@ class FrigateConfig(FrigateBaseModel): "live": ..., "objects": ..., "review": ..., - "genai": ..., "motion": ..., "notifications": ..., "detect": ..., @@ -506,7 +483,9 @@ class FrigateConfig(FrigateBaseModel): model_config["path"] = detector_config.model_path if "path" not in model_config: - if detector_config.type == "cpu": + 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" @@ -525,6 +504,7 @@ class FrigateConfig(FrigateBaseModel): 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: @@ -606,6 +586,9 @@ class FrigateConfig(FrigateBaseModel): # 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 @@ -619,6 +602,12 @@ class FrigateConfig(FrigateBaseModel): 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 + ) # Add default filters object_keys = camera_config.objects.track @@ -673,6 +662,13 @@ class FrigateConfig(FrigateBaseModel): # 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 live view stream if none is set @@ -685,7 +681,6 @@ class FrigateConfig(FrigateBaseModel): verify_config_roles(camera_config) verify_valid_live_stream_names(self, camera_config) - verify_recording_retention(camera_config) verify_recording_segments_setup_with_reasonable_time(camera_config) verify_zone_objects_are_tracked(camera_config) verify_required_zones_exist(camera_config) @@ -694,15 +689,46 @@ class FrigateConfig(FrigateBaseModel): verify_objects_track(camera_config, labelmap_objects) verify_lpr_and_face(self, camera_config) + # 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." + ) + if self.plus_api and not self.snapshots.clean_copy: logger.warning( "Frigate+ is configured but clean snapshots are not enabled, submissions to Frigate+ will not be possible./" ) + # 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") @@ -716,6 +742,7 @@ class FrigateConfig(FrigateBaseModel): @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. @@ -735,15 +762,14 @@ class FrigateConfig(FrigateBaseModel): 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" + "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, **context): + 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) @@ -767,6 +793,19 @@ class FrigateConfig(FrigateBaseModel): 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) diff --git a/frigate/config/env.py b/frigate/config/env.py index 0a9b92e8f..6534ff411 100644 --- a/frigate/config/env.py +++ b/frigate/config/env.py @@ -5,12 +5,13 @@ 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_")} -# read docker secret files as env vars too -if os.path.isdir("/run/secrets") and os.access("/run/secrets", os.R_OK): - for secret_file in os.listdir("/run/secrets"): +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("/run/secrets", secret_file)).read_text().strip() + Path(os.path.join(secrets_dir, secret_file)).read_text().strip() ) diff --git a/frigate/config/logger.py b/frigate/config/logger.py index e6e1c06d3..0ba3e6972 100644 --- a/frigate/config/logger.py +++ b/frigate/config/logger.py @@ -1,20 +1,11 @@ -import logging -from enum import Enum - from pydantic import Field, ValidationInfo, model_validator from typing_extensions import Self +from frigate.log import LogLevel, apply_log_levels + from .base import FrigateBaseModel -__all__ = ["LoggerConfig", "LogLevel"] - - -class LogLevel(str, Enum): - debug = "debug" - info = "info" - warning = "warning" - error = "error" - critical = "critical" +__all__ = ["LoggerConfig"] class LoggerConfig(FrigateBaseModel): @@ -26,16 +17,6 @@ class LoggerConfig(FrigateBaseModel): @model_validator(mode="after") def post_validation(self, info: ValidationInfo) -> Self: if isinstance(info.context, dict) and info.context.get("install", False): - logging.getLogger().setLevel(self.default.value.upper()) - - log_levels = { - "httpx": LogLevel.error, - "werkzeug": LogLevel.error, - "ws4py": LogLevel.error, - **self.logs, - } - - for log, level in log_levels.items(): - logging.getLogger(log).setLevel(level.value.upper()) + apply_log_levels(self.default.value.upper(), self.logs) return self diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py index cedd53734..a760d0a1f 100644 --- a/frigate/config/mqtt.py +++ b/frigate/config/mqtt.py @@ -30,7 +30,7 @@ class MqttConfig(FrigateBaseModel): ) tls_client_key: Optional[str] = Field(default=None, title="MQTT TLS Client Key") tls_insecure: Optional[bool] = Field(default=None, title="MQTT TLS Insecure") - qos: Optional[int] = Field(default=0, title="MQTT QoS") + qos: int = Field(default=0, title="MQTT QoS") @model_validator(mode="after") def user_requires_pass(self, info: ValidationInfo) -> Self: diff --git a/frigate/config/network.py b/frigate/config/network.py new file mode 100644 index 000000000..c8b3cfd1c --- /dev/null +++ b/frigate/config/network.py @@ -0,0 +1,13 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["IPv6Config", "NetworkingConfig"] + + +class IPv6Config(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable IPv6 for port 5000 and/or 8971") + + +class NetworkingConfig(FrigateBaseModel): + ipv6: IPv6Config = Field(default_factory=IPv6Config, title="Network configuration") diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py index 68bd400e7..a46b7b897 100644 --- a/frigate/config/proxy.py +++ b/frigate/config/proxy.py @@ -16,6 +16,10 @@ class HeaderMappingConfig(FrigateBaseModel): default=None, title="Header name from upstream proxy to identify user role.", ) + role_map: Optional[dict[str, list[str]]] = Field( + default_factory=dict, + title=("Mapping of Frigate roles to upstream group values. "), + ) class ProxyConfig(FrigateBaseModel): diff --git a/frigate/config/ui.py b/frigate/config/ui.py index 2f66aeed3..8e0d4d77d 100644 --- a/frigate/config/ui.py +++ b/frigate/config/ui.py @@ -37,9 +37,6 @@ class UIConfig(FrigateBaseModel): time_style: DateTimeStyleEnum = Field( default=DateTimeStyleEnum.medium, title="Override UI timeStyle." ) - strftime_fmt: Optional[str] = Field( - default=None, title="Override date and time format using strftime syntax." - ) unit_system: UnitSystemEnum = Field( default=UnitSystemEnum.metric, title="The unit system to use for measurements." ) diff --git a/frigate/const.py b/frigate/const.py index 699a194ac..41c24f087 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -11,6 +11,7 @@ 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" FRIGATE_LOCALHOST = "http://127.0.0.1:5000" @@ -73,8 +74,12 @@ 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_-]+$" @@ -109,11 +114,22 @@ 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" 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 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 000000000..82472ad62 --- /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=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 index aea6751a0..51ee64938 100644 --- a/frigate/data_processing/common/face/model.py +++ b/frigate/data_processing/common/face/model.py @@ -9,8 +9,9 @@ import numpy as np from scipy import stats from frigate.config import FrigateConfig -from frigate.const import MODEL_CACHE_DIR +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__) @@ -37,6 +38,7 @@ class FaceRecognizer(ABC): def classify(self, face_image: np.ndarray) -> tuple[str, float] | None: pass + @redirect_output_to_logger(logger, logging.DEBUG) def init_landmark_detector(self) -> None: landmark_model = os.path.join(MODEL_CACHE_DIR, "facedet/landmarkdet.yaml") @@ -170,7 +172,7 @@ class FaceNetRecognizer(FaceRecognizer): face_embeddings_map: dict[str, list[np.ndarray]] = {} idx = 0 - dir = "/media/frigate/clips/faces" + dir = FACE_DIR for name in os.listdir(dir): if name == "train": continue @@ -260,14 +262,14 @@ class FaceNetRecognizer(FaceRecognizer): score = confidence label = name - return label, round(score - blur_reduction, 2) + return label, max(0, round(score - blur_reduction, 2)) class ArcFaceRecognizer(FaceRecognizer): def __init__(self, config: FrigateConfig): super().__init__(config) self.mean_embs: dict[int, np.ndarray] = {} - self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding() + self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding(config.face_recognition) self.model_builder_queue: queue.Queue | None = None def clear(self) -> None: @@ -280,7 +282,7 @@ class ArcFaceRecognizer(FaceRecognizer): face_embeddings_map: dict[str, list[np.ndarray]] = {} idx = 0 - dir = "/media/frigate/clips/faces" + dir = FACE_DIR for name in os.listdir(dir): if name == "train": continue @@ -368,4 +370,4 @@ class ArcFaceRecognizer(FaceRecognizer): score = confidence label = name - return label, round(score - blur_reduction, 2) + 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 index 2c68ce374..b56c66a19 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -14,15 +14,15 @@ from typing import Any, List, Optional, Tuple import cv2 import numpy as np -from Levenshtein import distance, jaro_winkler 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.const import CLIPS_DIR +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE from frigate.types import TrackedObjectUpdateTypesEnum from frigate.util.builtin import EventsPerSecond, InferenceSpeed @@ -43,9 +43,23 @@ class LicensePlateProcessingMixin: self.plates_det_second = EventsPerSecond() self.plates_det_second.start() self.event_metadata_publisher = EventMetadataPublisher() - self.ctc_decoder = CTCDecoder() + 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 @@ -54,6 +68,7 @@ class LicensePlateProcessingMixin: # matching self.similarity_threshold = 0.8 + self.cluster_threshold = 0.85 def _detect(self, image: np.ndarray) -> List[np.ndarray]: """ @@ -198,7 +213,7 @@ class LicensePlateProcessingMixin: boxes = self._detect(image) if len(boxes) == 0: - logger.debug("No boxes found by OCR detector model") + logger.debug(f"{camera}: No boxes found by OCR detector model") return [], [], [] if len(boxes) > 0: @@ -348,6 +363,30 @@ class LicensePlateProcessingMixin: 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 = [ @@ -370,15 +409,22 @@ class LicensePlateProcessingMixin: ): if len(plate) < self.lpr_config.min_plate_length: logger.debug( - f"Filtered out '{plate}' due to length ({len(plate)} < {self.lpr_config.min_plate_length})" + f"{camera}: Filtered out '{plate}' due to length ({len(plate)} < {self.lpr_config.min_plate_length})" ) continue - if self.lpr_config.format and not re.fullmatch( - self.lpr_config.format, plate - ): - logger.debug(f"Filtered out '{plate}' due to format mismatch") - continue + if self.lpr_config.format: + try: + if not re.fullmatch(self.lpr_config.format, plate): + logger.debug( + f"{camera}: Filtered out '{plate}' due to format mismatch" + ) + continue + except re.error: + # Skip format filtering if regex is invalid + logger.error( + f"{camera}: Invalid regex in LPR format configuration: {self.lpr_config.format}" + ) filtered_data.append((plate, conf_list, area)) @@ -978,7 +1024,9 @@ class LicensePlateProcessingMixin: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) return image - def _detect_license_plate(self, input: np.ndarray) -> tuple[int, int, int, int]: + def _detect_license_plate( + self, camera: string, input: np.ndarray + ) -> tuple[int, int, int, int]: """ Use a lightweight YOLOv9 model to detect license plates for users without Frigate+ @@ -1048,118 +1096,89 @@ class LicensePlateProcessingMixin: ).clip(0, [input.shape[1], input.shape[0]] * 2) logger.debug( - f"Found license plate. Bounding box: {expanded_box.astype(int)}" + f"{camera}: Found license plate. Bounding box: {expanded_box.astype(int)}" ) return tuple(expanded_box.astype(int)) else: return None # No detection above the threshold - def _should_keep_previous_plate( - self, id, top_plate, top_char_confidences, top_area, avg_confidence - ): - """Determine if the previous plate should be kept over the current one.""" - if id not in self.detected_license_plates: - return False + 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 - prev_data = self.detected_license_plates[id] - prev_plate = prev_data["plate"] - prev_char_confidences = prev_data["char_confidences"] - prev_area = prev_data["area"] - prev_avg_confidence = ( - sum(prev_char_confidences) / len(prev_char_confidences) - if prev_char_confidences - else 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 = [] + 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): + return (len(c), max(v["conf"] for v in c)) + + best_cluster_idx = max( + range(len(clusters)), key=lambda j: cluster_score(clusters[j]) ) - - # 1. Normalize metrics - # Length score: Equal lengths = 0.5, penalize extra characters if low confidence - length_diff = len(top_plate) - len(prev_plate) - max_length_diff = 3 - curr_length_score = 0.5 + (length_diff / (2 * max_length_diff)) - curr_length_score = max(0, min(1, curr_length_score)) - prev_length_score = 0.5 - (length_diff / (2 * max_length_diff)) - prev_length_score = max(0, min(1, prev_length_score)) - - # Adjust length score based on confidence of extra characters - conf_threshold = 0.75 # Minimum confidence for a character to be "trusted" - top_plate_char_count = len(top_plate.replace(" ", "")) - prev_plate_char_count = len(prev_plate.replace(" ", "")) - - if top_plate_char_count > prev_plate_char_count: - extra_confidences = top_char_confidences[prev_plate_char_count:] - if extra_confidences: # Ensure the slice is not empty - extra_conf = min(extra_confidences) # Lowest extra char confidence - if extra_conf < conf_threshold: - curr_length_score *= extra_conf / conf_threshold # Penalize if weak - elif prev_plate_char_count > top_plate_char_count: - extra_confidences = prev_char_confidences[top_plate_char_count:] - if extra_confidences: # Ensure the slice is not empty - extra_conf = min(extra_confidences) - if extra_conf < conf_threshold: - prev_length_score *= extra_conf / conf_threshold - - # Area score: Normalize by max area - max_area = max(top_area, prev_area) - curr_area_score = top_area / max_area if max_area > 0 else 0 - prev_area_score = prev_area / max_area if max_area > 0 else 0 - - # Confidence scores - curr_conf_score = avg_confidence - prev_conf_score = prev_avg_confidence - - # Character confidence comparison (average over shared length) - min_length = min(len(top_plate), len(prev_plate)) - if min_length > 0: - curr_char_conf = sum(top_char_confidences[:min_length]) / min_length - prev_char_conf = sum(prev_char_confidences[:min_length]) / min_length - else: - curr_char_conf = prev_char_conf = 0 - - # Penalize any character below threshold - curr_min_conf = min(top_char_confidences) if top_char_confidences else 0 - prev_min_conf = min(prev_char_confidences) if prev_char_confidences else 0 - curr_conf_penalty = ( - 1.0 if curr_min_conf >= conf_threshold else (curr_min_conf / conf_threshold) - ) - prev_conf_penalty = ( - 1.0 if prev_min_conf >= conf_threshold else (prev_min_conf / conf_threshold) - ) - - # 2. Define weights (boost confidence importance) - weights = { - "length": 0.2, - "area": 0.2, - "avg_confidence": 0.35, - "char_confidence": 0.25, - } - - # 3. Calculate weighted scores with penalty - curr_score = ( - curr_length_score * weights["length"] - + curr_area_score * weights["area"] - + curr_conf_score * weights["avg_confidence"] - + curr_char_conf * weights["char_confidence"] - ) * curr_conf_penalty - - prev_score = ( - prev_length_score * weights["length"] - + prev_area_score * weights["area"] - + prev_conf_score * weights["avg_confidence"] - + prev_char_conf * weights["char_confidence"] - ) * prev_conf_penalty - - # 4. Log the comparison + best_cluster = clusters[best_cluster_idx] + best_size, best_max_conf = cluster_score(best_cluster) logger.debug( - f"Plate comparison - Current: {top_plate} (score: {curr_score:.3f}, min_conf: {curr_min_conf:.2f}) vs " - f"Previous: {prev_plate} (score: {prev_score:.3f}, min_conf: {prev_min_conf:.2f}) " - f"Metrics - Length: {len(top_plate)} vs {len(prev_plate)} (scores: {curr_length_score:.2f} vs {prev_length_score:.2f}), " - f"Area: {top_area} vs {prev_area}, " - f"Avg Conf: {avg_confidence:.2f} vs {prev_avg_confidence:.2f}, " - f"Char Conf: {curr_char_conf:.2f} vs {prev_char_conf:.2f}" + f" Selected best cluster {best_cluster_idx + 1}: size {best_size}, max conf {best_max_conf:.3f}" ) - # 5. Return True if previous plate scores higher - return prev_score > curr_score + # 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.""" @@ -1168,7 +1187,6 @@ class LicensePlateProcessingMixin: event_id = f"{now}-{rand_id}" self.event_metadata_publisher.publish( - EventMetadataTypeEnum.lpr_event_create, ( now, camera, @@ -1179,6 +1197,7 @@ class LicensePlateProcessingMixin: None, plate, ), + EventMetadataTypeEnum.lpr_event_create.value, ) return event_id @@ -1210,7 +1229,7 @@ class LicensePlateProcessingMixin: ) yolov9_start = datetime.datetime.now().timestamp() - license_plate = self._detect_license_plate(rgb) + 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" @@ -1250,7 +1269,7 @@ class LicensePlateProcessingMixin: # don't run for non car/motorcycle or non license plate (dedicated lpr with frigate+) objects if ( - obj_data.get("label") not in ["car", "motorcycle"] + obj_data.get("label") not in self.lp_objects and obj_data.get("label") != "license_plate" ): logger.debug( @@ -1258,21 +1277,39 @@ class LicensePlateProcessingMixin: ) return - # don't run for stationary car objects - if obj_data.get("stationary") == True: + # 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}: Not a processing license plate for a stationary car/motorcycle object." + 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)" ) return - # don't run for objects with no position changes - # this 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: - logger.debug( - f"{camera}: Plate detected in {self.config.cameras[camera].detect.min_initialized + 1} concurrent frames, LPR frame threshold ({self.config.cameras[camera].detect.min_initialized + 2})" - ) - 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: Optional[dict[str, Any]] = None @@ -1302,7 +1339,7 @@ class LicensePlateProcessingMixin: ) yolov9_start = datetime.datetime.now().timestamp() - license_plate = self._detect_license_plate(car) + 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" ) @@ -1342,7 +1379,7 @@ class LicensePlateProcessingMixin: logger.debug(f"{camera}: No attributes to parse.") return - if obj_data.get("label") in ["car", "motorcycle"]: + if obj_data.get("label") in self.lp_objects: attributes: list[dict[str, Any]] = obj_data.get( "current_attributes", [] ) @@ -1413,7 +1450,7 @@ class LicensePlateProcessingMixin: license_plate_frame, ) - logger.debug(f"{camera}: Running plate recognition.") + logger.debug(f"{camera}: Running plate recognition for id: {id}.") # run detection, returns results sorted by confidence, best first start = datetime.datetime.now().timestamp() @@ -1433,7 +1470,7 @@ class LicensePlateProcessingMixin: f"{camera}: Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)" ) else: - logger.debug("No text detected") + logger.debug(f"{camera}: No text detected") return top_plate, top_char_confidences, top_area = ( @@ -1468,7 +1505,7 @@ class LicensePlateProcessingMixin: and current_time - data["last_seen"] <= self.config.cameras[camera].lpr.expire_time ): - similarity = jaro_winkler(data["plate"], top_plate) + similarity = JaroWinkler.similarity(data["plate"], top_plate) if similarity >= self.similarity_threshold: plate_id = existing_id logger.debug( @@ -1476,9 +1513,7 @@ class LicensePlateProcessingMixin: ) break if plate_id is None: - plate_id = self._generate_plate_event( - obj_data, top_plate, avg_confidence - ) + 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}" ) @@ -1490,25 +1525,69 @@ class LicensePlateProcessingMixin: id = plate_id - # Check if we have a previously detected plate for this ID - if id in self.detected_license_plates: - if self._should_keep_previous_plate( - id, top_plate, top_char_confidences, top_area, avg_confidence - ): - logger.debug(f"{camera}: Keeping previous plate") - return + 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})" + ) + + # 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 in self.lpr_config.known_plates.items() + for label, plates_list in self.lpr_config.known_plates.items() if any( - re.match(f"^{plate}$", top_plate) - or distance(plate, top_plate) <= self.lpr_config.match_distance - for plate in plates + re.match(f"^{plate}$", rep_plate) + or Levenshtein.distance(plate, rep_plate) + <= self.lpr_config.match_distance + for plate in plates_list ) ), None, @@ -1517,12 +1596,11 @@ class LicensePlateProcessingMixin: logger.error( f"{camera}: Invalid regex in known plates configuration: {self.lpr_config.known_plates}" ) - sub_label = None # If it's a known plate, publish to sub_label if sub_label is not None: self.sub_label_publisher.publish( - EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence) + (id, sub_label, rep_conf), EventMetadataTypeEnum.sub_label.value ) # always publish to recognized_license_plate field @@ -1532,8 +1610,8 @@ class LicensePlateProcessingMixin: { "type": TrackedObjectUpdateTypesEnum.lpr, "name": sub_label, - "plate": top_plate, - "score": avg_confidence, + "plate": rep_plate, + "score": rep_conf, "id": id, "camera": camera, "timestamp": start, @@ -1541,8 +1619,8 @@ class LicensePlateProcessingMixin: ), ) self.sub_label_publisher.publish( - EventMetadataTypeEnum.recognized_license_plate, - (id, top_plate, avg_confidence), + (id, "recognized_license_plate", rep_plate, rep_conf), + EventMetadataTypeEnum.attribute.value, ) # save the best snapshot for dedicated lpr cams not using frigate+ @@ -1551,30 +1629,15 @@ class LicensePlateProcessingMixin: and "license_plate" not in self.config.cameras[camera].objects.track ): logger.debug( - f"{camera}: Writing snapshot for {id}, {top_plate}, {current_time}" + 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( - EventMetadataTypeEnum.save_lpr_snapshot, (base64.b64encode(encoded_img).decode("ASCII"), id, camera), + EventMetadataTypeEnum.save_lpr_snapshot.value, ) - if id not in self.detected_license_plates: - if camera not in self.camera_current_cars: - self.camera_current_cars[camera] = [] - - self.camera_current_cars[camera].append(id) - - self.detected_license_plates[id] = { - "plate": top_plate, - "char_confidences": top_char_confidences, - "area": top_area, - "obj_data": obj_data, - "camera": camera, - "last_seen": current_time if dedicated_lpr else None, - } - def handle_request(self, topic, request_data) -> dict[str, Any] | None: return @@ -1595,113 +1658,121 @@ class CTCDecoder: for each decoded character sequence. """ - def __init__(self): + def __init__(self, character_dict_path=None): """ - Initialize the CTCDecoder with a list of characters and a character map. + 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, "r", 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", + "{", + "|", + "}", + "~", + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + " ", + " ", + ] - The character set includes digits, letters, special characters, and a "blank" token - (used by the CTC model for decoding purposes). A character map is created to map - indices to characters. - """ - 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__( @@ -1735,7 +1806,7 @@ class CTCDecoder: merged_path.append(char_index) merged_probs.append(seq_log_probs[t, char_index]) - result = "".join(self.char_map[idx] for idx in merged_path) + result = "".join(self.char_map.get(idx, "") for idx in merged_path) results.append(result) confidence = np.exp(merged_probs).tolist() diff --git a/frigate/data_processing/post/api.py b/frigate/data_processing/post/api.py index cd6dda128..c341bd8ef 100644 --- a/frigate/data_processing/post/api.py +++ b/frigate/data_processing/post/api.py @@ -39,7 +39,9 @@ class PostProcessorApi(ABC): pass @abstractmethod - def handle_request(self, request_data: dict[str, Any]) -> dict[str, Any] | None: + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: """Handle metadata requests. Args: request_data (dict): containing data about requested change to process. diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py new file mode 100644 index 000000000..558ab433e --- /dev/null +++ b/frigate/data_processing/post/audio_transcription.py @@ -0,0 +1,218 @@ +"""Handle post-processing for audio transcription.""" + +import logging +import os +import threading +import time +from typing import Optional + +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.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, + 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 = 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) -> Optional[tuple[str, float]]: + """Transcribe WAV audio data using faster-whisper.""" + if not self.recognizer: + logger.debug("Recognizer not initialized") + return None + + try: + # 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/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py new file mode 100644 index 000000000..cdb5f4fc3 --- /dev/null +++ b/frigate/data_processing/post/object_descriptions.py @@ -0,0 +1,365 @@ +"""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 import GenAIClient +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 +from frigate.util.image import create_thumbnail, ensure_jpeg_bytes + +if TYPE_CHECKING: + from frigate.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, + client: GenAIClient, + semantic_trigger_processor: SemanticTriggerProcessor | None, + ): + super().__init__(config, metrics, None) + self.config = config + self.embeddings = embeddings + self.requestor = requestor + self.metrics = metrics + self.genai_client = client + 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 + threading.Thread( + target=self._genai_embed_description, + name=f"_genai_embed_description_{event.id}", + daemon=True, + args=( + event, + [ + data["thumbnail"] + for data in self.tracked_events[data["id"]] + ], + ), + ).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(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 + + if self.genai_client is None: + logger.error("GenAI not enabled") + return + + camera_config = self.config.cameras[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) + + # 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" + else ( + [data["thumbnail"] for data in self.tracked_events[event_id]] + if len(self.tracked_events.get(event_id, [])) > 0 + else [thumbnail] + ) + ) + + self._genai_embed_description(event, embed_image) + + 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 + + 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.""" + + snapshot_file = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg") + + if not os.path.isfile(snapshot_file): + logger.error( + f"Cannot load snapshot for {event.id}, file not found: {snapshot_file}" + ) + return None + + try: + with open(snapshot_file, "rb") as image_file: + snapshot_image = image_file.read() + + img = cv2.imdecode( + np.frombuffer(snapshot_image, dtype=np.int8), + cv2.IMREAD_COLOR, + ) + + # 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( + "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 + ) -> None: + if event.has_snapshot and camera_config.objects.genai.use_snapshot: + snapshot_image = self._read_and_crop_snapshot(event) + if not snapshot_image: + 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 + else ( + [data["thumbnail"] for data in self.tracked_events[event.id]] + 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[event.camera] + description = self.genai_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(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 000000000..0a2754468 --- /dev/null +++ b/frigate/data_processing/post/review_descriptions.py @@ -0,0 +1,561 @@ +"""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 CACHE_DIR, CLIPS_DIR, UPDATE_REVIEW_DESCRIPTION +from frigate.data_processing.types import PostProcessDataEnum +from frigate.genai import GenAIClient +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 + + +class ReviewDescriptionProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + client: GenAIClient, + ): + super().__init__(config, metrics, None) + self.requestor = requestor + self.metrics = metrics + self.genai_client = client + self.review_desc_speed = InferenceSpeed(self.metrics.review_desc_speed) + self.review_descs_dps = EventsPerSecond() + self.review_descs_dps.start() + + def calculate_frame_count( + self, + camera: str, + image_source: ImageSourceEnum = ImageSourceEnum.preview, + height: int = 480, + ) -> int: + """Calculate optimal number of frames based on context size, image source, and resolution. + + Token usage varies by resolution: larger images (ultrawide aspect ratios) use more tokens. + Estimates ~1 token per 1250 pixels. Targets 98% context utilization with safety margin. + Capped at 20 frames. + """ + context_size = self.genai_client.get_context_size() + camera_config = self.config.cameras[camera] + + detect_width = camera_config.detect.width + detect_height = camera_config.detect.height + 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) + + pixels_per_image = width * height + tokens_per_image = pixels_per_image / 1250 + prompt_tokens = 3800 + response_tokens = 300 + available_tokens = context_size - prompt_tokens - response_tokens + max_frames = int(available_tokens / tokens_per_image) + + return min(max(max_frames, 3), 20) + + def process_data(self, data, data_type): + self.metrics.review_desc_dps.value = self.review_descs_dps.eps() + + if data_type != PostProcessDataEnum.review: + 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) + + thumbs = self.get_recording_frames( + camera, + final_data["start_time"] - buffer_extension, + final_data["end_time"] + buffer_extension, + 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_descs_dps.update() + threading.Thread( + target=run_analysis, + args=( + self.requestor, + self.genai_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, request_data): + 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) + + return self.genai_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" + all_frames = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + 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) + + 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, 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) + 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 + + +@staticmethod +def run_analysis( + requestor: InterProcessRequestor, + genai_client: GenAIClient, + review_inference_speed: InferenceSpeed, + camera_config: CameraConfig, + final_data: dict[str, str], + 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 = titlecase(label.replace("_", " ")) + + if label in attribute_labels: + unified_objects.append(f"{object_type} (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 000000000..ec9e5d220 --- /dev/null +++ b/frigate/data_processing/post/semantic_trigger.py @@ -0,0 +1,269 @@ +"""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.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, + ): + 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"), "r") 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": + return + + thumbnail_bytes = get_event_thumbnail_bytes(event) + + 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, request_data): + return None + + def expire_object(self, object_id, camera): + pass diff --git a/frigate/data_processing/post/types.py b/frigate/data_processing/post/types.py new file mode 100644 index 000000000..44bb09fb0 --- /dev/null +++ b/frigate/data_processing/post/types.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, ConfigDict, Field + + +class ReviewMetadata(BaseModel): + model_config = ConfigDict(extra="ignore", protected_namespaces=()) + + title: str = Field(description="A concise title for the activity.") + scene: str = Field( + description="A comprehensive description of the setting and entities, including relevant context and plausible inferences if supported by visual evidence." + ) + shortSummary: str = Field( + description="A brief 2-sentence summary of the scene, suitable for notifications. Should capture the key activity and context without full detail." + ) + confidence: float = Field( + description="A float between 0 and 1 representing your overall confidence in this analysis." + ) + potential_threat_level: int = Field( + ge=0, + le=3, + description="An integer representing the potential threat level (1-3). 1: Minor anomaly. 2: Moderate concern. 3: High threat. Only include this field if a clear security concern is observable; otherwise, omit it.", + ) + 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/audio_transcription.py b/frigate/data_processing/real_time/audio_transcription.py new file mode 100644 index 000000000..2e6d599eb --- /dev/null +++ b/frigate/data_processing/real_time/audio_transcription.py @@ -0,0 +1,281 @@ +"""Handle processing audio for speech transcription using sherpa-onnx with FFmpeg pipe.""" + +import logging +import os +import queue +import threading +from typing import Optional + +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 = None + self.whisper_model = None + self.model_runner = model_runner + self.transcription_segments = [] + self.audio_queue = 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 + ) -> Optional[tuple[str, bool]]: + 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) -> None: + pass diff --git a/frigate/data_processing/real_time/bird.py b/frigate/data_processing/real_time/bird.py index d547f2ddd..7851c0997 100644 --- a/frigate/data_processing/real_time/bird.py +++ b/frigate/data_processing/real_time/bird.py @@ -13,6 +13,7 @@ from frigate.comms.event_metadata_updater import ( ) from frigate.config import FrigateConfig from frigate.const import MODEL_CACHE_DIR +from frigate.log import suppress_stderr_during from frigate.util.object import calculate_region from ..types import DataProcessorMetrics @@ -80,11 +81,13 @@ class BirdRealTimeProcessor(RealTimeProcessorApi): logger.error(f"Failed to download {path}: {e}") def __build_detector(self) -> None: - self.interpreter = Interpreter( - model_path=os.path.join(MODEL_CACHE_DIR, "bird/bird.tflite"), - num_threads=2, - ) - self.interpreter.allocate_tensors() + # 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() @@ -129,7 +132,11 @@ class BirdRealTimeProcessor(RealTimeProcessorApi): ] if input.shape != (224, 224): - input = cv2.resize(input, (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) @@ -157,8 +164,8 @@ class BirdRealTimeProcessor(RealTimeProcessorApi): return self.sub_label_publisher.publish( - EventMetadataTypeEnum.sub_label, (obj_data["id"], self.labelmap[best_id], score), + EventMetadataTypeEnum.sub_label.value, ) self.detected_birds[obj_data["id"]] = score 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 000000000..fac0ecc3d --- /dev/null +++ b/frigate/data_processing/real_time/custom_classification.py @@ -0,0 +1,673 @@ +"""Real time processor that works with classification tflite models.""" + +import datetime +import json +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, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import ( + CustomClassificationConfig, + ObjectClassificationType, +) +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR +from frigate.log import suppress_stderr_during +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed, load_labels +from frigate.util.object import box_overlaps, calculate_region + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + +logger = logging.getLogger(__name__) + +MAX_OBJECT_CLASSIFICATIONS = 16 + + +class CustomStateClassificationProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.model_config = model_config + 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 + self.tensor_input_details: dict[str, Any] | None = None + self.tensor_output_details: 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( + 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: + try: + from tflite_runtime.interpreter import Interpreter + except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + + 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) + 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): + 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 = 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 + + frame = rgb[y1:y2, x1:x2] + + try: + resized_frame = cv2.resize(frame, (224, 224)) + except Exception: + logger.warning("Failed to resize image for state classification") + return + + 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, + cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), + "none-none", + now, + "unknown", + 0.0, + max_files=save_attempts, + ) + 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 = np.argmax(probs) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - now) + + 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, + cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), + "none-none", + now, + 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.requestor.send_data( + f"{camera}/classification/{self.model_config.name}", + verified_state, + ) + + def handle_request(self, topic, request_data): + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + 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.", + } + else: + return None + else: + return None + + def expire_object(self, object_id, camera): + pass + + +class CustomObjectClassificationProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + sub_label_publisher: EventMetadataPublisher, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.model_config = model_config + 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 + self.sub_label_publisher = sub_label_publisher + self.requestor = requestor + self.tensor_input_details: dict[str, Any] | None = None + self.tensor_output_details: 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( + 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) + + 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] = [] + + self.classification_history[object_id].append( + (current_label, current_score, current_time) + ) + + history = self.classification_history[object_id] + + if len(history) < 3: + return None, 0.0 + + label_counts = {} + label_scores = {} + 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=label_counts.get) + best_count = label_counts[best_label] + + consensus_threshold = total_attempts * 0.6 + if best_count < consensus_threshold: + return None, 0.0 + + avg_score = sum(label_scores[best_label]) / len(label_scores[best_label]) + + if best_label == "none": + return None, 0.0 + + return best_label, avg_score + + def process_frame(self, obj_data, frame): + 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, + ] + + if crop.shape != (224, 224): + try: + resized_crop = cv2.resize(crop, (224, 224)) + except Exception: + logger.warning("Failed to resize image for state classification") + return + + 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, + cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), + object_id, + now, + "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, now)) + 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 = np.argmax(probs) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - now) + + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) + write_classification_attempt( + self.train_dir, + cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), + object_id, + now, + self.labelmap[best_id], + score, + max_files=save_attempts, + ) + + if score < self.model_config.threshold: + logger.debug(f"Score {score} is less than threshold.") + return + + sub_label = self.labelmap[best_id] + + consensus_label, consensus_score = self.get_weighted_score( + object_id, sub_label, score, now + ) + + if consensus_label is not None: + camera = obj_data["camera"] + + if ( + self.model_config.object_config.classification_type + == ObjectClassificationType.sub_label + ): + self.sub_label_publisher.publish( + (object_id, consensus_label, consensus_score), + EventMetadataTypeEnum.sub_label, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": now, + "model": self.model_config.name, + "sub_label": consensus_label, + "score": consensus_score, + } + ), + ) + elif ( + self.model_config.object_config.classification_type + == ObjectClassificationType.attribute + ): + self.sub_label_publisher.publish( + ( + object_id, + self.model_config.name, + consensus_label, + consensus_score, + ), + EventMetadataTypeEnum.attribute.value, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": now, + "model": self.model_config.name, + "attribute": consensus_label, + "score": consensus_score, + } + ), + ) + + def handle_request(self, topic, request_data): + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + logger.info( + f"Successfully loaded updated model for {self.model_config.name}" + ) + return { + "success": True, + "message": f"Loaded {self.model_config.name} model.", + } + else: + return None + else: + return None + + def expire_object(self, object_id, camera): + if object_id in self.classification_history: + self.classification_history.pop(object_id) + + +@staticmethod +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 index 5a6525362..1901a81e1 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -166,6 +166,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): 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() @@ -173,7 +174,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): # don't run for non person objects if obj_data.get("label") != "person": - logger.debug("Not a processing face for non person object.") + logger.debug("Not processing face for a non person object.") return # don't overwrite sub label for objects that have a sub label @@ -208,6 +209,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): person_box = obj_data.get("box") if not person_box: + logger.debug(f"No person box available for {id}") return rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) @@ -233,7 +235,8 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): try: face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR) - except Exception: + except Exception as e: + logger.debug(f"Failed to convert face frame color for {id}: {e}") return else: # don't run for object without attributes @@ -251,6 +254,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): # no faces detected in this frame if not face: + logger.debug(f"No face attributes found for {id}") return face_box = face.get("box") @@ -274,6 +278,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): 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 @@ -321,8 +326,8 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): if weighted_score >= self.face_config.recognition_threshold: self.sub_label_publisher.publish( - EventMetadataTypeEnum.sub_label, (id, weighted_sub_label, weighted_score), + EventMetadataTypeEnum.sub_label.value, ) self.__update_metrics(datetime.datetime.now().timestamp() - start) @@ -330,6 +335,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): def handle_request(self, topic, request_data) -> 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), @@ -417,7 +423,10 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): res = self.recognizer.classify(img) if not res: - return + return { + "message": "Model is still training, please try again in a few moments.", + "success": False, + } sub_label, score = res @@ -436,6 +445,13 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): ) 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, + } + def expire_object(self, object_id: str, camera: str): if object_id in self.person_face_history: self.person_face_history.pop(object_id) 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 000000000..024b19fba --- /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 index a19a856bf..263a8b987 100644 --- a/frigate/data_processing/types.py +++ b/frigate/data_processing/types.py @@ -1,9 +1,13 @@ """Embeddings types.""" -import multiprocessing as mp from enum import Enum +from multiprocessing.managers import SyncManager from multiprocessing.sharedctypes import Synchronized +import sherpa_onnx + +from frigate.data_processing.real_time.whisper_online import FasterWhisperASR + class DataProcessorMetrics: image_embeddings_speed: Synchronized @@ -16,18 +20,35 @@ class DataProcessorMetrics: alpr_pps: Synchronized yolov9_lpr_speed: Synchronized yolov9_lpr_pps: Synchronized + review_desc_speed: Synchronized + review_desc_dps: Synchronized + object_desc_speed: Synchronized + object_desc_dps: Synchronized + classification_speeds: dict[str, Synchronized] + classification_cps: dict[str, Synchronized] - def __init__(self): - self.image_embeddings_speed = mp.Value("d", 0.0) - self.image_embeddings_eps = mp.Value("d", 0.0) - self.text_embeddings_speed = mp.Value("d", 0.0) - self.text_embeddings_eps = mp.Value("d", 0.0) - self.face_rec_speed = mp.Value("d", 0.0) - self.face_rec_fps = mp.Value("d", 0.0) - self.alpr_speed = mp.Value("d", 0.0) - self.alpr_pps = mp.Value("d", 0.0) - self.yolov9_lpr_speed = mp.Value("d", 0.0) - self.yolov9_lpr_pps = mp.Value("d", 0.0) + 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: @@ -41,3 +62,6 @@ 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 index ccb75ae54..aa4928e84 100644 --- a/frigate/db/sqlitevecq.py +++ b/frigate/db/sqlitevecq.py @@ -1,3 +1,4 @@ +import re import sqlite3 from playhouse.sqliteq import SqliteQueueDatabase @@ -14,6 +15,10 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase): conn: sqlite3.Connection = super()._connect(*args, **kwargs) 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: @@ -21,6 +26,17 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase): 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) -> 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) diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py new file mode 100644 index 000000000..fcbb41e66 --- /dev/null +++ b/frigate/detectors/detection_runners.py @@ -0,0 +1,608 @@ +"""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__) + + +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(device in available_devices for device 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.yolov9_license_plate.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.facenet.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, + ] + + 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"]: + self.ov_core.set_property(device, {"PERFORMANCE_HINT": "LATENCY"}) + + # Compile model + 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 + + # Thread lock to prevent concurrent inference (needed for JinaV2 which shares + # one runner between text and vision embeddings called from different threads) + self._inference_lock = threading.Lock() + + 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 + """ + # Lock prevents concurrent access to infer_request + # Needed for JinaV2: genai thread (text) + embeddings thread (vision) + with self._inference_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") + 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) + 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 index 7ee04bde5..aa92f28f4 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -154,12 +154,12 @@ class ModelConfig(BaseModel): self.width = model_info["width"] self.height = model_info["height"] - self.input_tensor = model_info["inputShape"] - self.input_pixel_format = model_info["pixelFormat"] - self.model_type = model_info["type"] + 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 = model_info["inputDataType"] + self.input_dtype = InputDTypeEnum(model_info["inputDataType"]) # RKNN always uses NHWC if detector == "rknn": diff --git a/frigate/detectors/detector_utils.py b/frigate/detectors/detector_utils.py new file mode 100644 index 000000000..d732de871 --- /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 tensorflow.lite.python.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/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py index 8a54363e1..00351f519 100644 --- a/frigate/detectors/plugins/cpu_tfl.py +++ b/frigate/detectors/plugins/cpu_tfl.py @@ -1,11 +1,13 @@ import logging -import numpy as np from pydantic import Field from typing_extensions import Literal 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 @@ -27,39 +29,14 @@ class CpuTfl(DetectionApi): type_key = DETECTOR_KEY def __init__(self, detector_config: CpuDetectorConfig): - self.interpreter = Interpreter( - model_path=detector_config.model.path, - num_threads=detector_config.num_threads or 3, - ) + # 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, + ) - self.interpreter.allocate_tensors() - - self.tensor_input_details = self.interpreter.get_input_details() - self.tensor_output_details = self.interpreter.get_output_details() + tflite_init(self, interpreter) def 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 + return tflite_detect_raw(self, tensor_input) diff --git a/frigate/detectors/plugins/degirum.py b/frigate/detectors/plugins/degirum.py new file mode 100644 index 000000000..28a13389f --- /dev/null +++ b/frigate/detectors/plugins/degirum.py @@ -0,0 +1,139 @@ +import logging +import queue + +import numpy as np +from pydantic import Field +from typing_extensions import Literal + +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): + type: Literal[DETECTOR_KEY] + location: str = Field(default=None, title="Inference Location") + zoo: str = Field(default=None, title="Model Zoo") + token: str = Field(default=None, title="DeGirum Cloud Token") + + +### 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.") + + 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 index 246d2dd41..2b94fde39 100644 --- a/frigate/detectors/plugins/edgetpu_tfl.py +++ b/frigate/detectors/plugins/edgetpu_tfl.py @@ -1,19 +1,20 @@ import logging +import math import os +import cv2 import numpy as np from pydantic import Field from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi -from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum try: from tflite_runtime.interpreter import Interpreter, load_delegate except ModuleNotFoundError: from tensorflow.lite.python.interpreter import Interpreter, load_delegate - logger = logging.getLogger(__name__) DETECTOR_KEY = "edgetpu" @@ -26,6 +27,10 @@ class EdgeTpuDetectorConfig(BaseDetectorConfig): class EdgeTpuTfl(DetectionApi): type_key = DETECTOR_KEY + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yologeneric, + ] def __init__(self, detector_config: EdgeTpuDetectorConfig): device_config = {} @@ -63,31 +68,294 @@ class EdgeTpuTfl(DetectionApi): 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() - 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] - ) + 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 - detections = np.zeros((20, 6), np.float32) + # 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,) - 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], + 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] + ) - return detections + 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 index aa856dd80..cafc809c9 100755 --- a/frigate/detectors/plugins/hailo8l.py +++ b/frigate/detectors/plugins/hailo8l.py @@ -33,10 +33,6 @@ def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarr image = image[0] h, w = image.shape[:2] - - if (w, h) == (320, 320) and (model_w, model_h) == (640, 640): - return cv2.resize(image, (model_w, model_h), interpolation=cv2.INTER_LINEAR) - 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) diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py new file mode 100644 index 000000000..a93888f8a --- /dev/null +++ b/frigate/detectors/plugins/memryx.py @@ -0,0 +1,868 @@ +import glob +import logging +import os +import shutil +import urllib.request +import zipfile +from queue import Queue + +import cv2 +import numpy as np +from pydantic import BaseModel, Field +from typing_extensions import Literal + +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): + type: Literal[DETECTOR_KEY] + device: str = Field(default="PCIe", title="Device Path") + + +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." + ) + 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): + """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 index 45e37d6cd..6c9e510ce 100644 --- a/frigate/detectors/plugins/onnx.py +++ b/frigate/detectors/plugins/onnx.py @@ -5,12 +5,12 @@ from pydantic import Field from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import get_optimized_runner from frigate.detectors.detector_config import ( BaseDetectorConfig, ModelTypeEnum, ) from frigate.util.model import ( - get_ort_providers, post_process_dfine, post_process_rfdetr, post_process_yolo, @@ -33,31 +33,18 @@ class ONNXDetector(DetectionApi): def __init__(self, detector_config: ONNXDetectorConfig): super().__init__(detector_config) - try: - import onnxruntime as ort - - logger.info("ONNX: loaded onnxruntime module") - except ModuleNotFoundError: - logger.error( - "ONNX: module loading failed, need 'pip install onnxruntime'?!?" - ) - raise - path = detector_config.model.path logger.info(f"ONNX: loading {detector_config.model.path}") - providers, options = get_ort_providers( - detector_config.device == "CPU", detector_config.device - ) - - self.model = ort.InferenceSession( - path, providers=providers, provider_options=options + 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 - path = detector_config.model.path if self.onnx_model_type == ModelTypeEnum.yolox: self.calculate_grids_strides() @@ -66,19 +53,18 @@ class ONNXDetector(DetectionApi): def detect_raw(self, tensor_input: np.ndarray): if self.onnx_model_type == ModelTypeEnum.dfine: - tensor_output = self.model.run( - None, + 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.model.get_inputs()[0].name - tensor_output = self.model.run(None, {model_input_name: tensor_input}) + 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) diff --git a/frigate/detectors/plugins/openvino.py b/frigate/detectors/plugins/openvino.py index 066b6d311..bda5c8871 100644 --- a/frigate/detectors/plugins/openvino.py +++ b/frigate/detectors/plugins/openvino.py @@ -1,5 +1,4 @@ import logging -import os import numpy as np import openvino as ov @@ -7,6 +6,7 @@ from pydantic import Field from typing_extensions import Literal 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, @@ -37,20 +37,23 @@ class OvDetector(DetectionApi): def __init__(self, detector_config: OvDetectorConfig): super().__init__(detector_config) - self.ov_core = ov.Core() self.ov_model_type = detector_config.model.model_type self.h = detector_config.model.height self.w = detector_config.model.width - if not os.path.isfile(detector_config.model.path): - logger.error(f"OpenVino model file {detector_config.model.path} not found.") - raise FileNotFoundError - - self.interpreter = self.ov_core.compile_model( - model=detector_config.model.path, device_name=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: @@ -60,8 +63,8 @@ class OvDetector(DetectionApi): self.model_invalid = True if self.ov_model_type == ModelTypeEnum.ssd: - model_inputs = self.interpreter.inputs - model_outputs = self.interpreter.outputs + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs if len(model_inputs) != 1: logger.error( @@ -80,8 +83,8 @@ class OvDetector(DetectionApi): self.model_invalid = True if self.ov_model_type == ModelTypeEnum.yolonas: - model_inputs = self.interpreter.inputs - model_outputs = self.interpreter.outputs + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs if len(model_inputs) != 1: logger.error( @@ -104,7 +107,9 @@ class OvDetector(DetectionApi): self.output_indexes = 0 while True: try: - tensor_shape = self.interpreter.output(self.output_indexes).shape + tensor_shape = self.runner.compiled_model.output( + self.output_indexes + ).shape logger.info( f"Model Output-{self.output_indexes} Shape: {tensor_shape}" ) @@ -129,39 +134,33 @@ class OvDetector(DetectionApi): ] def detect_raw(self, tensor_input): - infer_request = self.interpreter.create_infer_request() - # TODO: see if we can use shared_memory=True - input_tensor = ov.Tensor(array=tensor_input) + if self.model_invalid: + return np.zeros((20, 6), np.float32) if self.ov_model_type == ModelTypeEnum.dfine: - infer_request.set_tensor("images", input_tensor) - target_sizes_tensor = ov.Tensor( - np.array([[self.h, self.w]], dtype=np.int64) - ) - infer_request.set_tensor("orig_target_sizes", target_sizes_tensor) - infer_request.infer() + # 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 = ( - infer_request.get_output_tensor(0).data, - infer_request.get_output_tensor(1).data, - infer_request.get_output_tensor(2).data, + outputs[0], + outputs[1], + outputs[2], ) return post_process_dfine(tensor_output, self.w, self.h) - infer_request.infer(input_tensor) + # 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.model_invalid: - return detections - elif self.ov_model_type == ModelTypeEnum.rfdetr: - return post_process_rfdetr( - [ - infer_request.get_output_tensor(0).data, - infer_request.get_output_tensor(1).data, - ] - ) + if self.ov_model_type == ModelTypeEnum.rfdetr: + return post_process_rfdetr(outputs) elif self.ov_model_type == ModelTypeEnum.ssd: - results = infer_request.get_output_tensor(0).data[0][0] + results = outputs[0][0][0] for i, (_, class_id, score, xmin, ymin, xmax, ymax) in enumerate(results): if i == 20: @@ -176,7 +175,7 @@ class OvDetector(DetectionApi): ] return detections elif self.ov_model_type == ModelTypeEnum.yolonas: - predictions = infer_request.get_output_tensor(0).data + predictions = outputs[0] for i, prediction in enumerate(predictions): if i == 20: @@ -195,16 +194,10 @@ class OvDetector(DetectionApi): ] return detections elif self.ov_model_type == ModelTypeEnum.yologeneric: - out_tensor = [] - - for item in infer_request.output_tensors: - out_tensor.append(item.data) - - return post_process_yolo(out_tensor, self.w, self.h) + return post_process_yolo(outputs, self.w, self.h) elif self.ov_model_type == ModelTypeEnum.yolox: - out_tensor = infer_request.get_output_tensor() # [x, y, h, w, box_score, class_no_1, ..., class_no_80], - results = out_tensor.data + 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, ...] diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py index 46fae3e62..c16df507e 100644 --- a/frigate/detectors/plugins/rknn.py +++ b/frigate/detectors/plugins/rknn.py @@ -8,17 +8,17 @@ import cv2 import numpy as np from pydantic import Field -from frigate.const import MODEL_CACHE_DIR +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_socs = ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] - supported_models = { ModelTypeEnum.yologeneric: "^frigate-fp16-yolov9-[cemst]$", ModelTypeEnum.yolonas: "^deci-fp16-yolonas_[sml]$", @@ -60,18 +60,18 @@ class Rknn(DetectionApi): "For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html" ) - from rknnlite.api import RKNNLite - - self.rknn = RKNNLite(verbose=False) - if self.rknn.load_rknn(model_props["path"]) != 0: - logger.error("Error initializing rknn model.") - if self.rknn.init_runtime(core_mask=core_mask) != 0: - logger.error( - "Error initializing rknn runtime. Do you run docker in privileged mode?" - ) + 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): - self.rknn.release() + if hasattr(self, "runner") and self.runner: + # The runner's __del__ method will handle cleanup + pass def get_soc(self): try: @@ -80,9 +80,9 @@ class Rknn(DetectionApi): except FileNotFoundError: raise Exception("Make sure to run docker in privileged mode.") - if soc not in supported_socs: + 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_socs}." + f"Your SoC is not supported. Your SoC is: {soc}. Currently these SoCs are supported: {SUPPORTED_RK_SOCS}." ) return soc @@ -94,7 +94,34 @@ class Rknn(DetectionApi): # user provided models should be a path and contain a "/" if "/" in model_path: model_props["preset"] = False - model_props["path"] = model_path + + # 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 @@ -281,9 +308,7 @@ class Rknn(DetectionApi): ) def detect_raw(self, tensor_input): - output = self.rknn.inference( - [ - 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 000000000..6181b16d7 --- /dev/null +++ b/frigate/detectors/plugins/synaptics.py @@ -0,0 +1,103 @@ +import logging +import os + +import numpy as np +from typing_extensions import Literal + +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): + 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 000000000..7e29d6630 --- /dev/null +++ b/frigate/detectors/plugins/teflon_tfl.py @@ -0,0 +1,38 @@ +import logging + +from typing_extensions import Literal + +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): + 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/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py new file mode 100644 index 000000000..cd397aefa --- /dev/null +++ b/frigate/detectors/plugins/zmq_ipc.py @@ -0,0 +1,331 @@ +import json +import logging +import os +from typing import Any, List + +import numpy as np +import zmq +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "zmq" + + +class ZmqDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + endpoint: str = Field( + default="ipc:///tmp/cache/zmq_detector", title="ZMQ IPC endpoint" + ) + request_timeout_ms: int = Field( + default=200, title="ZMQ request timeout in milliseconds" + ) + linger_ms: int = Field(default=0, title="ZMQ socket linger 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 index fbdc8d940..0a854fcfa 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -3,26 +3,24 @@ import base64 import json import logging -import multiprocessing as mp import os -import signal import threading from json.decoder import JSONDecodeError -from types import FrameType -from typing import Any, Optional, Union +from multiprocessing.synchronize import Event as MpEvent +from typing import Any, Union import regex from pathvalidate import ValidationError, sanitize_filename -from setproctitle import setproctitle from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor from frigate.config import FrigateConfig -from frigate.const import CONFIG_DIR, FACE_DIR +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, Recordings +from frigate.models import Event from frigate.util.builtin import serialize -from frigate.util.services import listen +from frigate.util.classification import kickoff_model_training +from frigate.util.process import FrigateProcess from .maintainer import EmbeddingMaintainer from .util import ZScoreNormalization @@ -30,40 +28,30 @@ from .util import ZScoreNormalization logger = logging.getLogger(__name__) -def manage_embeddings(config: FrigateConfig, metrics: DataProcessorMetrics) -> None: - stop_event = mp.Event() +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 receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: - stop_event.set() - - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) - - threading.current_thread().name = "process:embeddings_manager" - setproctitle("frigate.embeddings_manager") - listen() - - # 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] - db.bind(models) - - maintainer = EmbeddingMaintainer( - db, - config, - metrics, - stop_event, - ) - maintainer.start() + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = EmbeddingMaintainer( + self.config, + self.metrics, + self.stop_event, + ) + maintainer.start() class EmbeddingsContext: @@ -300,3 +288,34 @@ class EmbeddingsContext: 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 index 833ab9ab2..8d7bcd235 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -7,7 +7,8 @@ import os import threading import time -from numpy import ndarray +import numpy as np +from peewee import DoesNotExist, IntegrityError from PIL import Image from playhouse.shortcuts import model_to_dict @@ -16,15 +17,16 @@ 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 +from frigate.models import Event, Trigger from frigate.types import ModelStatusTypesEnum from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize -from frigate.util.path import get_event_thumbnail_bytes +from frigate.util.file import get_event_thumbnail_bytes from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding from .onnx.jina_v2_embedding import JinaV2Embedding @@ -107,9 +109,8 @@ class Embeddings: self.embedding = JinaV2Embedding( model_size=self.config.semantic_search.model_size, requestor=self.requestor, - device="GPU" - if self.config.semantic_search.model_size == "large" - else "CPU", + 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" @@ -126,7 +127,8 @@ class Embeddings: self.vision_embedding = JinaV1ImageEmbedding( model_size=config.semantic_search.model_size, requestor=self.requestor, - device="GPU" if config.semantic_search.model_size == "large" else "CPU", + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), ) def update_stats(self) -> None: @@ -167,7 +169,7 @@ class Embeddings: def embed_thumbnail( self, event_id: str, thumbnail: bytes, upsert: bool = True - ) -> ndarray: + ) -> np.ndarray: """Embed thumbnail and optionally insert into DB. @param: event_id in Events DB @@ -194,7 +196,7 @@ class Embeddings: def batch_embed_thumbnail( self, event_thumbs: dict[str, bytes], upsert: bool = True - ) -> list[ndarray]: + ) -> 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 @@ -244,7 +246,7 @@ class Embeddings: def embed_description( self, event_id: str, description: str, upsert: bool = True - ) -> ndarray: + ) -> np.ndarray: start = datetime.datetime.now().timestamp() embedding = self.text_embedding([description])[0] @@ -264,7 +266,7 @@ class Embeddings: def batch_embed_description( self, event_descriptions: dict[str, str], upsert: bool = True - ) -> ndarray: + ) -> np.ndarray: start = datetime.datetime.now().timestamp() # upsert embeddings one by one to avoid token limit embeddings = [] @@ -417,3 +419,225 @@ class Embeddings: 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/maintainer.py b/frigate/embeddings/maintainer.py index 86bc75737..1a0950cbb 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -3,19 +3,18 @@ import base64 import datetime import logging -import os import threading from multiprocessing.synchronize import Event as MpEvent -from pathlib import Path -from typing import Any, Optional +from typing import Any -import cv2 -import numpy as np from peewee import DoesNotExist -from playhouse.sqliteq import SqliteQueueDatabase +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.embeddings_updater import ( + EmbeddingsRequestEnum, + EmbeddingsResponder, +) from frigate.comms.event_metadata_updater import ( EventMetadataPublisher, EventMetadataSubscriber, @@ -27,37 +26,44 @@ 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.const import ( - CLIPS_DIR, - UPDATE_EVENT_DESCRIPTION, +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, ) 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 EventTypeEnum, RegenerateDescriptionEnum from frigate.genai import get_genai_client -from frigate.models import Event -from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.models import Event, Recordings, ReviewSegment, Trigger from frigate.util.builtin import serialize -from frigate.util.image import ( - SharedMemoryFrameManager, - calculate_region, - ensure_jpeg_bytes, -) -from frigate.util.path import get_event_thumbnail_bytes +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import SharedMemoryFrameManager from .embeddings import Embeddings @@ -71,15 +77,44 @@ class EmbeddingMaintainer(threading.Thread): def __init__( self, - db: SqliteQueueDatabase, config: FrigateConfig, - metrics: DataProcessorMetrics, + 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.object_genai, + CameraConfigUpdateEnum.review_genai, + CameraConfigUpdateEnum.semantic_search, + ], + ) + self.classification_config_subscriber = ConfigSubscriber( + "config/classification/custom/" + ) + + # 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) if config.semantic_search.enabled: self.embeddings = Embeddings(config, db, metrics) @@ -88,6 +123,9 @@ class EmbeddingMaintainer(threading.Thread): 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() @@ -98,13 +136,15 @@ class EmbeddingMaintainer(threading.Thread): EventMetadataTypeEnum.regenerate_description ) self.recordings_subscriber = RecordingsDataSubscriber( - RecordingsDataTypeEnum.recordings_available_through + RecordingsDataTypeEnum.saved ) - self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video) + 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]] = {} + self.genai_client = get_genai_client(config) # model runners to share between realtime and post processors if self.config.lpr.enabled: @@ -118,11 +158,13 @@ class EmbeddingMaintainer(threading.Thread): 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( @@ -143,9 +185,33 @@ class EmbeddingMaintainer(threading.Thread): ) ) + for model_config in self.config.classification.custom.values(): + 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 self.genai_client is not None and 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_client + ) + ) + if self.config.lpr.enabled: self.post_processors.append( LicensePlatePostProcessor( @@ -158,10 +224,43 @@ class EmbeddingMaintainer(threading.Thread): ) ) + if self.config.audio_transcription.enabled and 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 self.genai_client is not None and 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_client, + semantic_trigger_processor, + ) + ) + self.stop_event = stop_event - self.tracked_events: dict[str, list[Any]] = {} - self.early_request_sent: dict[str, bool] = {} - self.genai_client = get_genai_client(config) # recordings data self.recordings_available_through: dict[str, float] = {} @@ -169,14 +268,19 @@ class EmbeddingMaintainer(threading.Thread): 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_classification_config_updates() self._process_requests() self._process_updates() self._process_recordings_updates() - self._process_dedicated_lpr() + self._process_review_updates() + self._process_frame_updates() self._expire_dedicated_lpr() self._process_finalized() self._process_event_metadata() + self.config_updater.stop() + self.classification_config_subscriber.stop() self.event_subscriber.stop() self.event_end_subscriber.stop() self.recordings_subscriber.stop() @@ -187,6 +291,68 @@ class EmbeddingMaintainer(threading.Thread): self.requestor.stop() logger.info("Exiting embeddings maintenance...") + def _check_classification_config_updates(self) -> None: + """Check for classification config updates and add/remove processors.""" + topic, model_config = self.classification_config_subscriber.check_for_update() + + if topic: + model_name = topic.split("/")[-1] + + if model_config is None: + self.realtime_processors = [ + processor + for processor in self.realtime_processors + if not ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name + ) + ] + + logger.info( + f"Successfully removed classification processor for model: {model_name}" + ) + else: + self.config.classification.custom[model_name] = model_config + + # Check if processor already exists + for processor in self.realtime_processors: + if isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ): + if processor.model_config.name == model_name: + logger.debug( + f"Classification processor for model {model_name} already exists, skipping" + ) + return + + if model_config.state_config is not None: + processor = CustomStateClassificationProcessor( + self.config, model_config, self.requestor, self.metrics + ) + else: + processor = CustomObjectClassificationProcessor( + self.config, + model_config, + self.event_metadata_publisher, + self.requestor, + self.metrics, + ) + + self.realtime_processors.append(processor) + logger.info( + f"Added classification processor for model: {model_name} (type: {type(processor).__name__})" + ) + def _process_requests(self) -> None: """Process embeddings requests""" @@ -223,6 +389,7 @@ class EmbeddingMaintainer(threading.Thread): if resp is not None: return resp + logger.error(f"No processor handled the topic {topic}") return None except Exception as e: logger.error(f"Unable to handle embeddings request {e}", exc_info=True) @@ -238,7 +405,14 @@ class EmbeddingMaintainer(threading.Thread): source_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: @@ -246,8 +420,11 @@ class EmbeddingMaintainer(threading.Thread): camera_config = self.config.cameras[camera] - # no need to process updated objects if face recognition, lpr, genai are disabled - if not camera_config.genai.enabled and len(self.realtime_processors) == 0: + # 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 @@ -256,6 +433,7 @@ class EmbeddingMaintainer(threading.Thread): 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: @@ -264,60 +442,24 @@ class EmbeddingMaintainer(threading.Thread): ) 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) - # no need to save our own thumbnails if genai is not enabled - # or if the object has become stationary - if self.genai_client is not None and not data["stationary"]: - if data["id"] not in self.tracked_events: - self.tracked_events[data["id"]] = [] - - data["thumbnail"] = self._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 ( - self.genai_client is not None - and camera_config.genai.send_triggers.after_significant_updates - ): - if ( - len(self.tracked_events.get(data["id"], [])) - >= camera_config.genai.send_triggers.after_significant_updates - and data["id"] not in self.early_request_sent - ): - if data["has_clip"] and data["has_snapshot"]: - event: Event = Event.get(Event.id == data["id"]) - - if ( - not camera_config.genai.objects - or event.label in camera_config.genai.objects - ) and ( - not camera_config.genai.required_zones - or set(data["entered_zones"]) - & set(camera_config.genai.required_zones) - ): - logger.debug(f"{camera} sending early request to GenAI") - - self.early_request_sent[data["id"]] = True - threading.Thread( - target=self._genai_embed_description, - name=f"_genai_embed_description_{event.id}", - daemon=True, - args=( - event, - [ - data["thumbnail"] - for data in self.tracked_events[data["id"]] - ], - ), - ).start() + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.process_data( + { + "camera": camera, + "data": data, + "state": "update", + "yuv_frame": yuv_frame, + }, + PostProcessDataEnum.tracked_object, + ) self.frame_manager.close(frame_name) @@ -330,7 +472,28 @@ class EmbeddingMaintainer(threading.Thread): break event_id, camera, updated_db = ended - camera_config = self.config.cameras[camera] + + # 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: + continue + + # Skip the event if not an object + if event.data.get("type") != "object": + 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: @@ -354,48 +517,33 @@ class EmbeddingMaintainer(threading.Thread): }, 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, PostProcessDataEnum.event_id) - - # expire in realtime processors - for processor in self.realtime_processors: - processor.expire_object(event_id, camera) - - if updated_db: - try: - event: Event = Event.get(Event.id == event_id) - except DoesNotExist: - continue - - # Skip the event if not an object - if event.data.get("type") != "object": - continue - - # Extract valid thumbnail - thumbnail = get_event_thumbnail_bytes(event) - - # Embed the thumbnail - self._embed_thumbnail(event_id, thumbnail) - - # Run GenAI - if ( - camera_config.genai.enabled - and camera_config.genai.send_triggers.tracked_object_end - and self.genai_client is not None - and ( - not camera_config.genai.objects - or event.label in camera_config.genai.objects + processor.process_data( + {"event_id": event_id, "camera": camera}, + PostProcessDataEnum.tracked_object, ) - and ( - not camera_config.genai.required_zones - or set(event.zones) & set(camera_config.genai.required_zones) - ) - ): - self._process_genai_description(event, camera_config, thumbnail) - - # Delete tracked events based on the event_id - if event_id in self.tracked_events: - del self.tracked_events[event_id] def _expire_dedicated_lpr(self) -> None: """Remove plates not seen for longer than expiration timeout for dedicated lpr cameras.""" @@ -412,28 +560,48 @@ class EmbeddingMaintainer(threading.Thread): to_remove.append(id) for id in to_remove: self.event_metadata_publisher.publish( - EventMetadataTypeEnum.manual_event_end, (id, now), + EventMetadataTypeEnum.manual_event_end.value, ) self.detected_license_plates.pop(id) def _process_recordings_updates(self) -> None: """Process recordings updates.""" while True: - recordings_data = self.recordings_subscriber.check_for_update() + update = self.recordings_subscriber.check_for_update() - if recordings_data == None: + if not update: break - camera, recordings_available_through_timestamp = recordings_data + (raw_topic, payload) = update - self.recordings_available_through[camera] = ( - recordings_available_through_timestamp - ) + if not raw_topic or not payload: + break - logger.debug( - f"{camera} now has recordings available through {recordings_available_through_timestamp}" - ) + 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 @@ -442,14 +610,21 @@ class EmbeddingMaintainer(threading.Thread): if topic is None: return - event_id, source = payload + event_id, source, force = payload if event_id: - self.handle_regenerate_description( - event_id, RegenerateDescriptionEnum(source) - ) + 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_dedicated_lpr(self) -> None: + def _process_frame_updates(self) -> None: """Process event updates""" (topic, data) = self.detection_subscriber.check_for_update() @@ -458,16 +633,17 @@ class EmbeddingMaintainer(threading.Thread): camera, frame_name, _, _, motion_boxes, _ = data - if not camera or not self.config.lpr.enabled or len(motion_boxes) == 0: + if not camera or len(motion_boxes) == 0 or camera not in self.config.cameras: return camera_config = self.config.cameras[camera] + dedicated_lpr_enabled = ( + camera_config.type == CameraTypeEnum.lpr + and "license_plate" not in camera_config.objects.track + ) - if ( - camera_config.type != CameraTypeEnum.lpr - or "license_plate" in camera_config.objects.track - ): - # we're not a dedicated lpr camera or we are one but we're using frigate+ + if not dedicated_lpr_enabled and len(self.config.classification.custom) == 0: + # no active features that use this data return try: @@ -484,195 +660,21 @@ class EmbeddingMaintainer(threading.Thread): return for processor in self.realtime_processors: - if isinstance(processor, LicensePlateRealTimeProcessor): + if dedicated_lpr_enabled 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 _create_thumbnail(self, yuv_frame, box, height=500) -> Optional[bytes]: - """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 _embed_thumbnail(self, event_id: str, thumbnail: bytes) -> None: """Embed the thumbnail for an event.""" if not self.config.semantic_search.enabled: return self.embeddings.embed_thumbnail(event_id, thumbnail) - - def _process_genai_description(self, event, camera_config, thumbnail) -> None: - if event.has_snapshot and camera_config.genai.use_snapshot: - snapshot_image = self._read_and_crop_snapshot(event, camera_config) - if not snapshot_image: - 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.genai.use_snapshot - else ( - [data["thumbnail"] for data in self.tracked_events[event.id]] - if num_thumbnails > 0 - else [thumbnail] - ) - ) - - if camera_config.genai.debug_save_thumbnails and num_thumbnails > 0: - logger.debug(f"Saving {num_thumbnails} thumbnails for event {event.id}") - - Path(os.path.join(CLIPS_DIR, f"genai-requests/{event.id}")).mkdir( - parents=True, exist_ok=True - ) - - for idx, data in enumerate(self.tracked_events[event.id], 1): - jpg_bytes: bytes = data["thumbnail"] - - if jpg_bytes is None: - logger.warning(f"Unable to save thumbnail {idx} for {event.id}.") - else: - with open( - os.path.join( - CLIPS_DIR, - f"genai-requests/{event.id}/{idx}.jpg", - ), - "wb", - ) as j: - j.write(jpg_bytes) - - # Generate the description. Call happens in a thread since it is network bound. - threading.Thread( - target=self._genai_embed_description, - name=f"_genai_embed_description_{event.id}", - daemon=True, - args=( - event, - embed_image, - ), - ).start() - - def _genai_embed_description(self, event: Event, thumbnails: list[bytes]) -> None: - """Embed the description for an event.""" - camera_config = self.config.cameras[event.camera] - - description = self.genai_client.generate_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(event.id, description) - - logger.debug( - "Generated description for %s (%d images): %s", - event.id, - len(thumbnails), - description, - ) - - def _read_and_crop_snapshot(self, event: Event, camera_config) -> bytes | None: - """Read, decode, and crop the snapshot image.""" - - snapshot_file = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg") - - if not os.path.isfile(snapshot_file): - logger.error( - f"Cannot load snapshot for {event.id}, file not found: {snapshot_file}" - ) - return None - - try: - with open(snapshot_file, "rb") as image_file: - snapshot_image = image_file.read() - - img = cv2.imdecode( - np.frombuffer(snapshot_image, dtype=np.int8), - cv2.IMREAD_COLOR, - ) - - # 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( - "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 handle_regenerate_description(self, event_id: str, source: str) -> None: - 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[event.camera] - if not camera_config.genai.enabled or self.genai_client is None: - logger.error(f"GenAI not enabled for camera {event.camera}") - return - - thumbnail = get_event_thumbnail_bytes(event) - - # 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, camera_config) - if not snapshot_image: - return - - embed_image = ( - [snapshot_image] - if event.has_snapshot and source == "snapshot" - else ( - [data["thumbnail"] for data in self.tracked_events[event_id]] - if len(self.tracked_events.get(event_id, [])) > 0 - else [thumbnail] - ) - ) - - self._genai_embed_description(event, embed_image) diff --git a/frigate/embeddings/onnx/base_embedding.py b/frigate/embeddings/onnx/base_embedding.py index fcadd2852..c0bd58475 100644 --- a/frigate/embeddings/onnx/base_embedding.py +++ b/frigate/embeddings/onnx/base_embedding.py @@ -3,7 +3,6 @@ import logging import os from abc import ABC, abstractmethod -from enum import Enum from io import BytesIO from typing import Any @@ -18,11 +17,6 @@ from frigate.util.downloader import ModelDownloader logger = logging.getLogger(__name__) -class EmbeddingTypeEnum(str, Enum): - thumbnail = "thumbnail" - description = "description" - - class BaseEmbedding(ABC): """Base embedding class.""" diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py index eb04b43b2..04d756897 100644 --- a/frigate/embeddings/onnx/face_embedding.py +++ b/frigate/embeddings/onnx/face_embedding.py @@ -6,10 +6,13 @@ 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 -from .runner import ONNXModelRunner try: from tflite_runtime.interpreter import Interpreter @@ -59,11 +62,13 @@ class FaceNetEmbedding(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = Interpreter( - model_path=os.path.join(MODEL_CACHE_DIR, "facedet/facenet.tflite"), - num_threads=2, - ) - self.runner.allocate_tensors() + # 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() @@ -110,7 +115,7 @@ class FaceNetEmbedding(BaseEmbedding): class ArcfaceEmbedding(BaseEmbedding): - def __init__(self): + def __init__(self, config: FaceRecognitionConfig): GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") super().__init__( model_name="facedet", @@ -119,6 +124,7 @@ class ArcfaceEmbedding(BaseEmbedding): "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 @@ -146,9 +152,10 @@ class ArcfaceEmbedding(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), - "GPU", + device=self.config.device or "GPU", + model_type=EnrichmentModelTypeEnum.arcface.value, ) def _preprocess_inputs(self, raw_inputs): diff --git a/frigate/embeddings/onnx/jina_v1_embedding.py b/frigate/embeddings/onnx/jina_v1_embedding.py index b448ec816..5e3ee7f3b 100644 --- a/frigate/embeddings/onnx/jina_v1_embedding.py +++ b/frigate/embeddings/onnx/jina_v1_embedding.py @@ -2,21 +2,24 @@ import logging import os +import threading import warnings -# 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 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 -from .runner import ONNXModelRunner warnings.filterwarnings( "ignore", @@ -52,6 +55,7 @@ class JinaV1TextEmbedding(BaseEmbedding): 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( @@ -125,24 +129,25 @@ class JinaV1TextEmbedding(BaseEmbedding): clean_up_tokenization_spaces=True, ) - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.jina_v1.value, ) def _preprocess_inputs(self, raw_inputs): - 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 - ] + 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): @@ -171,7 +176,8 @@ class JinaV1ImageEmbedding(BaseEmbedding): self.device = device self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) self.feature_extractor = None - self.runner: ONNXModelRunner | None = 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 @@ -184,6 +190,9 @@ class JinaV1ImageEmbedding(BaseEmbedding): 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( @@ -204,15 +213,16 @@ class JinaV1ImageEmbedding(BaseEmbedding): f"{MODEL_CACHE_DIR}/{self.model_name}", ) - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.jina_v1.value, ) def _preprocess_inputs(self, raw_inputs): - 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 - ] + 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 index e9def9a07..1abd968c9 100644 --- a/frigate/embeddings/onnx/jina_v2_embedding.py +++ b/frigate/embeddings/onnx/jina_v2_embedding.py @@ -3,6 +3,7 @@ import io import logging import os +import threading import numpy as np from PIL import Image @@ -11,11 +12,12 @@ 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 -from .runner import ONNXModelRunner # disables the progress bar and download logging for downloading tokenizers and image processors disable_progress_bar() @@ -52,6 +54,11 @@ class JinaV2Embedding(BaseEmbedding): 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 @@ -64,6 +71,9 @@ class JinaV2Embedding(BaseEmbedding): 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( @@ -125,10 +135,10 @@ class JinaV2Embedding(BaseEmbedding): clean_up_tokenization_spaces=True, ) - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.jina_v2.value, ) def _preprocess_image(self, image_data: bytes | Image.Image) -> np.ndarray: @@ -196,37 +206,40 @@ class JinaV2Embedding(BaseEmbedding): def __call__( self, inputs: list[str] | list[Image.Image] | list[str], embedding_type=None ) -> list[np.ndarray]: - self.embedding_type = embedding_type - if not self.embedding_type: - raise ValueError( - "embedding_type must be specified either in __init__ or __call__" - ) + # 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) + 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") + # 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") + # 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] + 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 index 35ff5ceee..ad2099957 100644 --- a/frigate/embeddings/onnx/lpr_embedding.py +++ b/frigate/embeddings/onnx/lpr_embedding.py @@ -7,11 +7,12 @@ 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 -from .runner import ONNXModelRunner warnings.filterwarnings( "ignore", @@ -32,21 +33,23 @@ class PaddleOCRDetection(BaseEmbedding): device: str = "AUTO", ): model_file = ( - "detection-large.onnx" if model_size == "large" else "detection-small.onnx" + "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/{model_file}" + 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: ONNXModelRunner | None = None + 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 @@ -75,10 +78,10 @@ class PaddleOCRDetection(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.paddleocr.value, ) def _preprocess_inputs(self, raw_inputs): @@ -107,7 +110,7 @@ class PaddleOCRClassification(BaseEmbedding): self.model_size = model_size self.device = device self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) - self.runner: ONNXModelRunner | None = None + 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 @@ -136,10 +139,10 @@ class PaddleOCRClassification(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.paddleocr.value, ) def _preprocess_inputs(self, raw_inputs): @@ -159,16 +162,17 @@ class PaddleOCRRecognition(BaseEmbedding): GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") super().__init__( model_name="paddleocr-onnx", - model_file="recognition.onnx", + model_file="recognition_v4.onnx", download_urls={ - "recognition.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/recognition.onnx" + "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: ONNXModelRunner | None = None + 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 @@ -197,10 +201,10 @@ class PaddleOCRRecognition(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.paddleocr.value, ) def _preprocess_inputs(self, raw_inputs): @@ -230,7 +234,7 @@ class LicensePlateDetector(BaseEmbedding): self.model_size = model_size self.device = device self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) - self.runner: ONNXModelRunner | None = None + 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 @@ -259,10 +263,10 @@ class LicensePlateDetector(BaseEmbedding): if self.downloader: self.downloader.wait_for_download() - self.runner = ONNXModelRunner( + self.runner = get_optimized_runner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, + model_type=EnrichmentModelTypeEnum.yolov9_license_plate.value, ) def _preprocess_inputs(self, raw_inputs): diff --git a/frigate/embeddings/onnx/runner.py b/frigate/embeddings/onnx/runner.py deleted file mode 100644 index c34c97a8d..000000000 --- a/frigate/embeddings/onnx/runner.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Convenience runner for onnx models.""" - -import logging -import os.path -from typing import Any - -import onnxruntime as ort - -from frigate.const import MODEL_CACHE_DIR -from frigate.util.model import get_ort_providers - -try: - import openvino as ov -except ImportError: - # openvino is not included - pass - -logger = logging.getLogger(__name__) - - -class ONNXModelRunner: - """Run onnx models optimally based on available hardware.""" - - def __init__(self, model_path: str, device: str, requires_fp16: bool = False): - self.model_path = model_path - self.ort: ort.InferenceSession = None - self.ov: ov.Core = None - providers, options = get_ort_providers(device == "CPU", device, requires_fp16) - self.interpreter = None - - if "OpenVINOExecutionProvider" in providers: - try: - # use OpenVINO directly - self.type = "ov" - self.ov = ov.Core() - self.ov.set_property( - {ov.properties.cache_dir: os.path.join(MODEL_CACHE_DIR, "openvino")} - ) - self.interpreter = self.ov.compile_model( - model=model_path, device_name=device - ) - except Exception as e: - logger.warning( - f"OpenVINO failed to build model, using CPU instead: {e}" - ) - self.interpreter = None - - # Use ONNXRuntime - if self.interpreter is None: - self.type = "ort" - self.ort = ort.InferenceSession( - model_path, - providers=providers, - provider_options=options, - ) - - def get_input_names(self) -> list[str]: - if self.type == "ov": - input_names = [] - - for input in self.interpreter.inputs: - input_names.extend(input.names) - - return input_names - elif self.type == "ort": - return [input.name for input in self.ort.get_inputs()] - - def get_input_width(self): - """Get the input width of the model regardless of backend.""" - if self.type == "ort": - return self.ort.get_inputs()[0].shape[3] - elif self.type == "ov": - input_info = self.interpreter.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 - return -1 - - def run(self, input: dict[str, Any]) -> Any: - if self.type == "ov": - infer_request = self.interpreter.create_infer_request() - - try: - # This ensures the model starts with a clean state for each sequence - # Important for RNN models like PaddleOCR recognition - infer_request.reset_state() - except Exception: - # this will raise an exception for models with AUTO set as the device - pass - - outputs = infer_request.infer(input) - - return outputs - elif self.type == "ort": - return self.ort.run(None, input) diff --git a/frigate/embeddings/types.py b/frigate/embeddings/types.py new file mode 100644 index 000000000..32cbe5dd0 --- /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/events/audio.py b/frigate/events/audio.py index f2a217fd3..e88f2ae71 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -2,35 +2,42 @@ import datetime import logging -import random -import string import threading import time -from typing import Any, Tuple +from multiprocessing.managers import DictProxy +from multiprocessing.synchronize import Event as MpEvent +from typing import Tuple import numpy as np -import frigate.util as util -from frigate.camera import CameraMetrics -from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum -from frigate.comms.event_metadata_updater import ( - EventMetadataPublisher, - EventMetadataTypeEnum, -) from frigate.comms.inter_process import InterProcessRequestor -from frigate.config import CameraConfig, CameraInput, FfmpegConfig +from frigate.config import CameraConfig, CameraInput, FfmpegConfig, FrigateConfig +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 +from frigate.log import LogPipe, suppress_stderr_during from frigate.object_detection.base import load_labels from frigate.util.builtin import get_ffmpeg_arg_list +from frigate.util.process import FrigateProcess from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg try: @@ -39,6 +46,9 @@ except ModuleNotFoundError: from tensorflow.lite.python.interpreter import Interpreter +logger = logging.getLogger(__name__) + + def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> 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) + ( @@ -67,31 +77,47 @@ def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> list[str]: ) -class AudioProcessor(util.Process): +class AudioProcessor(FrigateProcess): name = "frigate.audio_manager" def __init__( self, + config: FrigateConfig, cameras: list[CameraConfig], - camera_metrics: dict[str, CameraMetrics], + camera_metrics: DictProxy, + stop_event: MpEvent, ): - super().__init__(name="frigate.audio_manager", daemon=True) + super().__init__( + stop_event, PROCESS_PRIORITY_HIGH, name="frigate.audio_manager", daemon=True + ) self.camera_metrics = camera_metrics self.cameras = cameras + self.config = config def run(self) -> None: + self.pre_run_setup(self.config.logger) audio_threads: list[AudioEventMaintainer] = [] threading.current_thread().name = "process:audio_manager" + if self.config.audio_transcription.enabled: + self.transcription_model_runner = AudioTranscriptionModelRunner( + self.config.audio_transcription.device, + self.config.audio_transcription.model_size, + ) + else: + self.transcription_model_runner = None + if len(self.cameras) == 0: return for camera in self.cameras: audio_thread = AudioEventMaintainer( camera, + self.config, self.camera_metrics, + self.transcription_model_runner, self.stop_event, ) audio_threads.append(audio_thread) @@ -119,76 +145,121 @@ class AudioEventMaintainer(threading.Thread): def __init__( self, camera: CameraConfig, - camera_metrics: dict[str, CameraMetrics], + 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 = camera + self.config = config + self.camera_config = camera self.camera_metrics = camera_metrics - self.detections: dict[dict[str, Any]] = {} self.stop_event = stop_event - self.detector = AudioTfl(stop_event, self.config.audio.num_threads) + 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.config.name}") - self.ffmpeg_cmd = get_ffmpeg_command(self.config.ffmpeg) - self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.audio") + 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 = 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 = ConfigSubscriber(f"config/audio/{camera.name}") - self.enabled_subscriber = ConfigSubscriber( - f"config/enabled/{camera.name}", True + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {self.camera_config.name: self.camera_config}, + [ + CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.audio_transcription, + ], ) - self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio) - self.event_metadata_publisher = EventMetadataPublisher() + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value) + + if self.config.audio_transcription.enabled: + # 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 def detect_audio(self, audio) -> None: - if not self.config.audio.enabled or self.stop_event.is_set(): + if not self.camera_config.audio.enabled or self.stop_event.is_set(): return audio_as_float = audio.astype(np.float32) rms, dBFS = self.calculate_audio_levels(audio_as_float) - self.camera_metrics[self.config.name].audio_rms.value = rms - self.camera_metrics[self.config.name].audio_dBFS.value = dBFS + 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.config.audio.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) - audio_detections = [] for label, score, _ in model_detections: self.logger.debug( - f"{self.config.name} heard {label} with a score of {score}" + f"{self.camera_config.name} heard {label} with a score of {score}" ) - if label not in self.config.audio.listen: + if label not in self.camera_config.audio.listen: continue - if score > dict((self.config.audio.filters or {}).get(label, {})).get( - "threshold", 0.8 - ): - self.handle_detection(label, score) - audio_detections.append(label) + 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.config.name, + self.camera_config.name, datetime.datetime.now().timestamp(), dBFS, - audio_detections, + [label for label, _ in audio_detections], ) ) - self.expire_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.float32) -> Tuple[float, float]: # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude @@ -201,78 +272,11 @@ class AudioEventMaintainer(threading.Thread): else: dBFS = 0 - self.requestor.send_data(f"{self.config.name}/audio/dBFS", float(dBFS)) - self.requestor.send_data(f"{self.config.name}/audio/rms", float(rms)) + 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 handle_detection(self, label: str, score: float) -> None: - if self.detections.get(label): - self.detections[label]["last_detection"] = ( - datetime.datetime.now().timestamp() - ) - else: - now = datetime.datetime.now().timestamp() - rand_id = "".join( - random.choices(string.ascii_lowercase + string.digits, k=6) - ) - event_id = f"{now}-{rand_id}" - self.requestor.send_data(f"{self.config.name}/audio/{label}", "ON") - - self.event_metadata_publisher.publish( - EventMetadataTypeEnum.manual_event_create, - ( - now, - self.config.name, - label, - event_id, - True, - score, - None, - None, - "audio", - {}, - ), - ) - self.detections[label] = { - "id": event_id, - "label": label, - "last_detection": now, - } - - def expire_detections(self) -> None: - now = datetime.datetime.now().timestamp() - - for detection in self.detections.values(): - if not detection: - continue - - if ( - now - detection.get("last_detection", now) - > self.config.audio.max_not_heard - ): - self.requestor.send_data( - f"{self.config.name}/audio/{detection['label']}", "OFF" - ) - - self.event_metadata_publisher.publish( - EventMetadataTypeEnum.manual_event_end, - (detection["id"], detection["last_detection"]), - ) - self.detections[detection["label"]] = None - - def expire_all_detections(self) -> None: - """Immediately end all current detections""" - now = datetime.datetime.now().timestamp() - for label, detection in list(self.detections.items()): - if detection: - self.requestor.send_data(f"{self.config.name}/audio/{label}", "OFF") - self.event_metadata_publisher.publish( - EventMetadataTypeEnum.manual_event_end, - (detection["id"], now), - ) - self.detections[label] = None - def start_or_restart_ffmpeg(self) -> None: self.audio_listener = start_or_restart_ffmpeg( self.ffmpeg_cmd, @@ -281,13 +285,14 @@ class AudioEventMaintainer(threading.Thread): 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.config.ffmpeg.retry_interval) + time.sleep(self.camera_config.ffmpeg.retry_interval) self.logpipe.dump() self.start_or_restart_ffmpeg() @@ -296,6 +301,9 @@ class AudioEventMaintainer(threading.Thread): 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 @@ -308,32 +316,28 @@ class AudioEventMaintainer(threading.Thread): self.logger.error(f"Error reading audio data from ffmpeg process: {e}") log_and_restart() - def _update_enabled_state(self) -> bool: - """Fetch the latest config and update enabled state.""" - _, config_data = self.enabled_subscriber.check_for_update() - if config_data: - self.config.enabled = config_data.enabled - return config_data.enabled - - return self.config.enabled - def run(self) -> None: - if self._update_enabled_state(): + if self.camera_config.enabled: self.start_or_restart_ffmpeg() while not self.stop_event.is_set(): - enabled = self._update_enabled_state() + enabled = self.camera_config.enabled if enabled != self.was_enabled: if enabled: self.logger.debug( - f"Enabling audio detections for {self.config.name}" + f"Enabling audio detections for {self.camera_config.name}" ) self.start_or_restart_ffmpeg() else: - self.logger.debug( - f"Disabling audio detections for {self.config.name}, ending events" + 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 ) - self.expire_all_detections() stop_ffmpeg(self.audio_listener, self.logger) self.audio_listener = None self.was_enabled = enabled @@ -344,22 +348,21 @@ class AudioEventMaintainer(threading.Thread): continue # check if there is an updated config - ( - updated_topic, - updated_audio_config, - ) = self.config_subscriber.check_for_update() - - if updated_topic: - self.config.audio = updated_audio_config + self.config_subscriber.check_for_updates() 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.enabled_subscriber.stop() self.detection_publisher.stop() @@ -368,12 +371,13 @@ class AudioTfl: self.stop_event = stop_event self.num_threads = num_threads self.labels = load_labels("/audio-labelmap.txt", prefill=521) - self.interpreter = Interpreter( - model_path="/cpu_audio_model.tflite", - num_threads=self.num_threads, - ) - - self.interpreter.allocate_tensors() + # 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() diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index 1e97ca14c..1ac03b2ed 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -12,7 +12,7 @@ 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.path import delete_event_snapshot, delete_event_thumbnail +from frigate.util.file import delete_event_snapshot, delete_event_thumbnail logger = logging.getLogger(__name__) @@ -229,6 +229,11 @@ class EventCleanup(threading.Thread): 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" ) diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 2b0fc4193..0d1a1b025 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -46,7 +46,7 @@ def should_update_state(prev_event: Event, current_event: Event) -> bool: if prev_event["sub_label"] != current_event["sub_label"]: return True - if len(prev_event["current_zones"]) < len(current_event["current_zones"]): + if set(prev_event["current_zones"]) != set(current_event["current_zones"]): return True return False diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index a26efae3e..43272a6d1 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -7,6 +7,7 @@ from typing import Any from frigate.const import ( FFMPEG_HVC1_ARGS, + FFMPEG_HWACCEL_AMF, FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_RKMPP, FFMPEG_HWACCEL_VAAPI, @@ -22,35 +23,51 @@ logger = logging.getLogger(__name__) class LibvaGpuSelector: "Automatically selects the correct libva GPU." - _selected_gpu = None + _valid_gpus: list[str] | None = None - def get_selected_gpu(self) -> str: - """Get selected libva GPU.""" + def __get_valid_gpus(self) -> None: + """Get valid libva GPUs.""" if not os.path.exists("/dev/dri"): - return "" + self._valid_gpus = [] + return - if self._selected_gpu: - return self._selected_gpu + if self._valid_gpus: + return devices = list(filter(lambda d: d.startswith("render"), os.listdir("/dev/dri"))) if not devices: - return "/dev/dri/renderD128" + self._valid_gpus = ["/dev/dri/renderD128"] + return if len(devices) < 2: - self._selected_gpu = f"/dev/dri/{devices[0]}" - return self._selected_gpu + 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._selected_gpu = f"/dev/dri/{device}" - return self._selected_gpu + self._valid_gpus.append(f"/dev/dri/{device}") - return "" + 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" @@ -62,18 +79,21 @@ _user_agent_args = [ 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: f"-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format vaapi", - "preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -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 {_gpu_selector.get_selected_gpu()} -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 cuda -hwaccel_output_format cuda", + 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 @@ -95,6 +115,8 @@ 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}", @@ -108,6 +130,7 @@ PRESETS_HW_ACCEL_SCALE = { "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 @@ -122,6 +145,8 @@ PRESETS_HW_ACCEL_SCALE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( 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}", @@ -133,6 +158,7 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { "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"] = ( @@ -149,6 +175,8 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_BIR 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}", @@ -161,6 +189,7 @@ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { "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"] = ( @@ -185,6 +214,7 @@ def parse_preset_hardware_acceleration_decode( fps: int, width: int, height: int, + gpu: int, ) -> list[str]: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): @@ -195,7 +225,8 @@ def parse_preset_hardware_acceleration_decode( if not decode: return None - return decode.format(fps, width, height).split(" ") + gpu_arg = _gpu_selector.get_gpu_arg(arg, gpu) + return decode.format(fps, width, height, gpu_arg).split(" ") def parse_preset_hardware_acceleration_scale( @@ -215,7 +246,7 @@ def parse_preset_hardware_acceleration_scale( ",hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5" in scale and os.environ.get("FFMPEG_DISABLE_GAMMA_EQUALIZER") is not None ): - scale.replace( + scale = scale.replace( ",hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5", ":format=nv12,hwdownload,format=nv12,format=yuv420p", ) @@ -257,7 +288,7 @@ def parse_preset_hardware_acceleration_encode( ffmpeg_path, input, output, - _gpu_selector.get_selected_gpu(), + _gpu_selector.get_gpu_arg(arg, 0), ) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index a3fc7a09c..7f0192912 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -1,13 +1,17 @@ """Generative AI module for Frigate.""" +import datetime import importlib import logging import os -from typing import Optional +import re +from typing import Any, Optional from playhouse.shortcuts import model_to_dict from frigate.config import CameraConfig, FrigateConfig, GenAIConfig, GenAIProviderEnum +from frigate.const import CLIPS_DIR +from frigate.data_processing.post.types import ReviewMetadata from frigate.models import Event logger = logging.getLogger(__name__) @@ -28,12 +32,236 @@ def register_genai_provider(key: GenAIProviderEnum): class GenAIClient: """Generative AI client for Frigate.""" - def __init__(self, genai_config: GenAIConfig, timeout: int = 60) -> None: + def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None: self.genai_config: GenAIConfig = genai_config self.timeout = timeout self.provider = self._init_provider() - def generate_description( + 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.""" + + def get_concern_prompt() -> str: + if concerns: + concern_list = "\n - ".join(concerns) + return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring: + - {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)" + + context_prompt = f""" +Your task is to analyze the sequence of images ({len(thumbnails)} total) taken in chronological order from the perspective of the {review_data["camera"]} security camera. + +## Normal Activity Patterns for This Property + +{activity_context_prompt} + +## Task Instructions + +Your task is to provide a clear, accurate description of the scene that: +1. States exactly what is happening based on observable actions and movements. +2. Evaluates the activity against the Normal and Suspicious Activity Indicators above. +3. Assigns a potential_threat_level (0, 1, or 2) based on the threat level indicators defined above, applying them consistently. + +**Use the activity patterns above as guidance to calibrate your assessment. Match the activity against both normal and suspicious indicators, then use your judgment based on the complete context.** + +## Analysis Guidelines + +When forming your description: +- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list. +- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence. +- 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 Format + +Your response MUST be a flat JSON object with: +- `title` (string): A concise, direct title that describes the primary action or event in the sequence, not just what you literally see. Use spatial context when available to make titles more meaningful. When multiple objects/actions are present, prioritize whichever is most prominent or occurs first. Use names from "Objects in Scene" based on what you visually observe. If you see both a name and an unidentified object of the same type but visually observe only one person/object, use ONLY the name. Examples: "Joe walking dog", "Person taking out trash", "Vehicle arriving in driveway", "Joe accessing vehicle", "Person leaving porch for driveway". +- `scene` (string): A narrative description of what happens across the sequence from start to finish, in chronological order. Start by describing how the sequence begins, then describe the progression of events. **Describe all significant movements and actions in the order they occur.** For example, if a vehicle arrives and then a person exits, describe both actions sequentially. **Only describe actions you can actually observe happening in the frames provided.** Do not infer or assume actions that aren't visible (e.g., if you see someone walking but never see them sit, don't say they sat down). Include setting, detected objects, and their observable actions. Avoid speculation or filling in assumed behaviors. Your description should align with and support the threat level you assign. +- `shortSummary` (string): A brief 2-sentence summary of the scene, suitable for notifications. Should capture the key activity and context without full detail. This should be a condensed version of the scene description above. +- `confidence` (float): 0-1 confidence in your analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. Lower confidence when the sequence is unclear, objects are partially obscured, or context is ambiguous. +- `potential_threat_level` (integer): 0, 1, or 2 as defined in "Normal Activity Patterns for This Property" above. Your threat level must be consistent with your scene description and the guidance above. +{get_concern_prompt()} + +## Sequence Details + +- 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. Parentheses indicate object type or category, use only the name/label in your response, not the parentheses. + +**CRITICAL: When you see both recognized and unrecognized entries of the same type (e.g., "Joe (person)" and "Person"), visually count how many distinct people/objects you actually see based on appearance and clothing. If you observe only ONE person throughout the sequence, use ONLY the recognized name (e.g., "Joe"). The same person may be recognized in some frames but not others. Only describe both if you visually see MULTIPLE distinct people with clearly different appearances.** + +**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.** +{get_objects_list()} + +## Important Notes +- Values must be plain strings, floats, or integers — no nested objects, no extra commentary. +- Only describe objects from the "Objects in Scene" list above. Do not hallucinate additional objects. +- When describing people or vehicles, use the exact names provided. +{get_language_prompt()} +""" + 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 = self._send(context_prompt, thumbnails) + + 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) + + # If any verified objects (contain parentheses with name), set to 0 + if any("(" in obj for obj in review_data["unified_objects"]): + metadata.potential_threat_level = 0 + + metadata.time = review_data["start"] + return metadata + except Exception as e: + # rarely LLMs can fail to follow directions on output format + logger.warning( + f"Failed to parse review description as the response did not match expected format. {e}" + ) + return None + else: + 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.""" + time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}" + timeline_summary_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 +""" + + timeline_summary_prompt += "\n\nEvents:\n" + for event in events: + timeline_summary_prompt += f"\n{event}\n" + + if preferred_language: + timeline_summary_prompt += f"\nProvide your answer in {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], @@ -41,9 +269,9 @@ class GenAIClient: ) -> Optional[str]: """Generate a description for the frame.""" try: - prompt = camera_config.genai.object_prompts.get( + prompt = camera_config.objects.genai.object_prompts.get( event.label, - camera_config.genai.prompt, + camera_config.objects.genai.prompt, ).format(**model_to_dict(event)) except KeyError as e: logger.error(f"Invalid key in GenAI prompt: {e}") @@ -60,19 +288,20 @@ class GenAIClient: """Submit a request to the provider.""" return None + def get_context_size(self) -> int: + """Get the context window size for this provider in tokens.""" + return 4096 + def get_genai_client(config: FrigateConfig) -> Optional[GenAIClient]: """Get the GenAI client.""" - genai_config = config.genai - genai_cameras = [ - c for c in config.cameras.values() if c.enabled and c.genai.enabled - ] + if not config.genai.provider: + return None - if genai_cameras: - load_providers() - provider = PROVIDERS.get(genai_config.provider) - if provider: - return provider(genai_config) + load_providers() + provider = PROVIDERS.get(config.genai.provider) + if provider: + return provider(config.genai) return None diff --git a/frigate/genai/azure-openai.py b/frigate/genai/azure-openai.py index 155fa2431..eb08f7786 100644 --- a/frigate/genai/azure-openai.py +++ b/frigate/genai/azure-openai.py @@ -64,6 +64,7 @@ class OpenAIClient(GenAIClient): }, ], timeout=self.timeout, + **self.genai_config.runtime_options, ) except Exception as e: logger.warning("Azure OpenAI returned an error: %s", str(e)) @@ -71,3 +72,7 @@ class OpenAIClient(GenAIClient): if len(result.choices) > 0: return result.choices[0].message.content.strip() return None + + def get_context_size(self) -> int: + """Get the context window size for Azure OpenAI.""" + return 128000 diff --git a/frigate/genai/gemini.py b/frigate/genai/gemini.py index 750454e25..83bd3340d 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/gemini.py @@ -3,8 +3,8 @@ import logging from typing import Optional -import google.generativeai as genai -from google.api_core.exceptions import GoogleAPICallError +from google import genai +from google.genai import errors, types from frigate.config import GenAIProviderEnum from frigate.genai import GenAIClient, register_genai_provider @@ -16,38 +16,64 @@ logger = logging.getLogger(__name__) class GeminiClient(GenAIClient): """Generative AI client for Frigate using Gemini.""" - provider: genai.GenerativeModel + provider: genai.Client def _init_provider(self): """Initialize the client.""" - genai.configure(api_key=self.genai_config.api_key) - return genai.GenerativeModel(self.genai_config.model) + # Merge provider_options into HttpOptions + http_options_dict = { + "api_version": "v1", + "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]) -> Optional[str]: """Submit a request to Gemini.""" - data = [ - { - "mime_type": "image/jpeg", - "data": img, - } - for img in images + contents = [ + types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images ] + [prompt] try: - response = self.provider.generate_content( - data, - generation_config=genai.types.GenerationConfig( - candidate_count=1, - ), - request_options=genai.types.RequestOptions( - timeout=self.timeout, + # Merge runtime_options into generation_config if provided + generation_config_dict = {"candidate_count": 1} + generation_config_dict.update(self.genai_config.runtime_options) + + response = self.provider.models.generate_content( + model=self.genai_config.model, + contents=contents, + config=types.GenerateContentConfig( + **generation_config_dict, ), ) - except GoogleAPICallError as e: + 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: description = response.text.strip() - except ValueError: + except (ValueError, AttributeError): # No description was generated return None return description + + def get_context_size(self) -> int: + """Get the context window size for Gemini.""" + # Gemini Pro Vision has a 1M token context window + return 1000000 diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index e67d532f0..ab6d3c0b3 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -1,9 +1,9 @@ """Ollama Provider for Frigate AI.""" import logging -from typing import Optional +from typing import Any, Optional -from httpx import TimeoutException +from httpx import RemoteProtocolError, TimeoutException from ollama import Client as ApiClient from ollama import ResponseError @@ -17,10 +17,24 @@ logger = logging.getLogger(__name__) 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 + provider_options: dict[str, Any] def _init_provider(self): """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) # ensure the model is available locally @@ -44,12 +58,31 @@ class OllamaClient(GenAIClient): ) return None try: + ollama_options = { + **self.provider_options, + **self.genai_config.runtime_options, + } result = self.provider.generate( self.genai_config.model, prompt, - images=images, + images=images if images else None, + **ollama_options, + ) + logger.debug( + f"Ollama tokens used: eval_count={result.get('eval_count')}, prompt_eval_count={result.get('prompt_eval_count')}" ) return result["response"].strip() - except (TimeoutException, ResponseError) as e: + except ( + TimeoutException, + ResponseError, + RemoteProtocolError, + ConnectionError, + ) as e: logger.warning("Ollama returned an error: %s", str(e)) return None + + def get_context_size(self) -> int: + """Get the context window size for Ollama.""" + return self.genai_config.provider_options.get("options", {}).get( + "num_ctx", 4096 + ) diff --git a/frigate/genai/openai.py b/frigate/genai/openai.py index 76ba8cb44..1fb0dd852 100644 --- a/frigate/genai/openai.py +++ b/frigate/genai/openai.py @@ -18,10 +18,18 @@ class OpenAIClient(GenAIClient): """Generative AI client for Frigate using OpenAI.""" provider: OpenAI + context_size: Optional[int] = None def _init_provider(self): """Initialize the client.""" - return OpenAI(api_key=self.genai_config.api_key) + # 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" + } + return OpenAI(api_key=self.genai_config.api_key, **provider_opts) def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: """Submit a request to OpenAI.""" @@ -53,6 +61,7 @@ class OpenAIClient(GenAIClient): }, ], timeout=self.timeout, + **self.genai_config.runtime_options, ) if ( result is not None @@ -64,3 +73,46 @@ class OpenAIClient(GenAIClient): except (TimeoutException, Exception) as e: logger.warning("OpenAI returned an error: %s", str(e)) return None + + 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 diff --git a/frigate/log.py b/frigate/log.py index 096b52215..5cec0e0d8 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -1,14 +1,18 @@ # In log.py import atexit +import io import logging -import multiprocessing as mp import os import sys import threading from collections import deque +from contextlib import contextmanager +from enum import Enum +from functools import wraps from logging.handlers import QueueHandler, QueueListener -from queue import Queue -from typing import Deque, Optional +from multiprocessing.managers import SyncManager +from queue import Empty, Queue +from typing import Any, Callable, Deque, Generator, Optional from frigate.util.builtin import clean_camera_user_pass @@ -33,14 +37,21 @@ LOG_HANDLER.addFilter( not in record.getMessage() ) + +class LogLevel(str, Enum): + debug = "debug" + info = "info" + warning = "warning" + error = "error" + critical = "critical" + + log_listener: Optional[QueueListener] = None log_queue: Optional[Queue] = None -manager = None -def setup_logging() -> None: - global log_listener, log_queue, manager - manager = mp.Manager() +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) @@ -57,13 +68,33 @@ def setup_logging() -> None: def _stop_logging() -> None: - global log_listener, manager + global log_listener if log_listener is not None: log_listener.stop() log_listener = None - if manager is not None: - manager.shutdown() - manager = 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 @@ -81,11 +112,11 @@ 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): + 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 = logging.ERROR + self.level = level self.deque: Deque[str] = deque(maxlen=100) self.fdRead, self.fdWrite = os.pipe() self.pipeReader = os.fdopen(self.fdRead) @@ -114,3 +145,210 @@ class LogPipe(threading.Thread): 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 index 5aa0dc5b2..93f6cb54f 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -1,6 +1,8 @@ from peewee import ( + BlobField, BooleanField, CharField, + CompositeKey, DateTimeField, FloatField, ForeignKeyField, @@ -11,7 +13,7 @@ from peewee import ( from playhouse.sqlite_ext import JSONField -class Event(Model): # type: ignore[misc] +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) @@ -49,7 +51,7 @@ class Event(Model): # type: ignore[misc] data = JSONField() # ex: tracked object box, region, etc. -class Timeline(Model): # type: ignore[misc] +class Timeline(Model): timestamp = DateTimeField() camera = CharField(index=True, max_length=20) source = CharField(index=True, max_length=20) # ex: tracked object, audio, external @@ -58,13 +60,13 @@ class Timeline(Model): # type: ignore[misc] data = JSONField() # ex: tracked object id, region, box, etc. -class Regions(Model): # type: ignore[misc] +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): # type: ignore[misc] +class Recordings(Model): id = CharField(null=False, primary_key=True, max_length=30) camera = CharField(index=True, max_length=20) path = CharField(unique=True) @@ -78,7 +80,7 @@ class Recordings(Model): # type: ignore[misc] regions = IntegerField(null=True) -class Export(Model): # type: ignore[misc] +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) @@ -88,7 +90,7 @@ class Export(Model): # type: ignore[misc] in_progress = BooleanField() -class ReviewSegment(Model): # type: ignore[misc] +class ReviewSegment(Model): id = CharField(null=False, primary_key=True, max_length=30) camera = CharField(index=True, max_length=20) start_time = DateTimeField() @@ -98,7 +100,7 @@ class ReviewSegment(Model): # type: ignore[misc] data = JSONField() # additional data about detection like list of labels, zone, areas of significant motion -class UserReviewStatus(Model): # type: ignore[misc] +class UserReviewStatus(Model): user_id = CharField(max_length=30) review_segment = ForeignKeyField(ReviewSegment, backref="user_reviews") has_been_reviewed = BooleanField(default=False) @@ -107,7 +109,7 @@ class UserReviewStatus(Model): # type: ignore[misc] indexes = ((("user_id", "review_segment"), True),) -class Previews(Model): # type: ignore[misc] +class Previews(Model): id = CharField(null=False, primary_key=True, max_length=30) camera = CharField(index=True, max_length=20) path = CharField(unique=True) @@ -117,18 +119,46 @@ class Previews(Model): # type: ignore[misc] # Used for temporary table in record/cleanup.py -class RecordingsToDelete(Model): # type: ignore[misc] +class RecordingsToDelete(Model): id = CharField(null=False, primary_key=False, max_length=30) class Meta: temporary = True -class User(Model): # type: ignore[misc] +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 index db5f25879..1f6785d5d 100644 --- a/frigate/motion/__init__.py +++ b/frigate/motion/__init__.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from typing import Tuple +from numpy import ndarray + from frigate.config import MotionConfig @@ -18,13 +20,21 @@ class MotionDetector(ABC): pass @abstractmethod - def detect(self, frame): + def detect(self, frame: ndarray) -> list: + """Detect motion and return motion boxes.""" pass @abstractmethod def is_calibrating(self): + """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): + """Stop any ongoing work and processes.""" pass diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index 69de6d015..b081d3791 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -5,7 +5,6 @@ import numpy as np from scipy.ndimage import gaussian_filter from frigate.camera import PTZMetrics -from frigate.comms.config_updater import ConfigSubscriber from frigate.config import MotionConfig from frigate.motion import MotionDetector from frigate.util.image import grab_cv2_contours @@ -36,12 +35,7 @@ class ImprovedMotionDetector(MotionDetector): self.avg_frame = np.zeros(self.motion_frame_size, np.float32) self.motion_frame_count = 0 self.frame_counter = 0 - resized_mask = cv2.resize( - config.mask, - dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), - interpolation=cv2.INTER_AREA, - ) - self.mask = np.where(resized_mask == [0]) + self.update_mask() self.save_images = False self.calibrating = True self.blur_radius = blur_radius @@ -49,7 +43,6 @@ class ImprovedMotionDetector(MotionDetector): self.contrast_values = np.zeros((contrast_frame_history, 2), np.uint8) self.contrast_values[:, 1:2] = 255 self.contrast_values_index = 0 - self.config_subscriber = ConfigSubscriber(f"config/motion/{name}", True) self.ptz_metrics = ptz_metrics self.last_stop_time = None @@ -59,12 +52,6 @@ class ImprovedMotionDetector(MotionDetector): def detect(self, frame): motion_boxes = [] - # check for updated motion config - _, updated_motion_config = self.config_subscriber.check_for_update() - - if updated_motion_config: - self.config = updated_motion_config - if not self.config.enabled: return motion_boxes @@ -244,6 +231,20 @@ class ImprovedMotionDetector(MotionDetector): return motion_boxes + def update_mask(self) -> None: + resized_mask = cv2.resize( + self.config.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.""" - self.config_subscriber.stop() + pass diff --git a/frigate/mypy.ini b/frigate/mypy.ini index c687a254d..5bad10f49 100644 --- a/frigate/mypy.ini +++ b/frigate/mypy.ini @@ -35,6 +35,9 @@ disallow_untyped_calls = false [mypy-frigate.const] ignore_errors = false +[mypy-frigate.comms.*] +ignore_errors = false + [mypy-frigate.events] ignore_errors = false @@ -50,6 +53,9 @@ ignore_errors = false [mypy-frigate.stats] ignore_errors = false +[mypy-frigate.track.*] +ignore_errors = false + [mypy-frigate.types] ignore_errors = false diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index c77a720a0..d2a54afbc 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -1,18 +1,22 @@ import datetime import logging -import multiprocessing as mp -import os import queue -import signal 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 import numpy as np -from setproctitle import setproctitle +import zmq -import frigate.util as util +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, @@ -21,7 +25,7 @@ from frigate.detectors.detector_config import ( ) from frigate.util.builtin import EventsPerSecond, load_labels from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory -from frigate.util.services import listen +from frigate.util.process import FrigateProcess from .util import tensor_transform @@ -34,11 +38,12 @@ class ObjectDetector(ABC): pass -class LocalObjectDetector(ObjectDetector): +class BaseLocalDetector(ObjectDetector): def __init__( self, detector_config: BaseDetectorConfig = None, labels: str = None, + stop_event: MpEvent = None, ): self.fps = EventsPerSecond() if labels is None: @@ -56,6 +61,22 @@ class LocalObjectDetector(ObjectDetector): 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=0.4): detections = [] @@ -73,76 +94,219 @@ class LocalObjectDetector(ObjectDetector): self.fps.update() return detections + +class LocalObjectDetector(BaseLocalDetector): def detect_raw(self, tensor_input: 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) - + tensor_input = self._transform_input(tensor_input) return self.detect_api.detect_raw(tensor_input=tensor_input) -def run_detector( - name: str, - detection_queue: Queue, - out_events: dict[str, MpEvent], - avg_speed: Value, - start: Value, - detector_config: BaseDetectorConfig, -): - threading.current_thread().name = f"detector:{name}" - logger = logging.getLogger(f"detector.{name}") - logger.info(f"Starting detection process: {os.getpid()}") - setproctitle(f"frigate.detector.{name}") - listen() +class AsyncLocalObjectDetector(BaseLocalDetector): + def async_send_input(self, tensor_input: np.ndarray, connection_id: str): + tensor_input = self._transform_input(tensor_input) + return self.detect_api.send_input(connection_id, tensor_input) - stop_event: MpEvent = mp.Event() + def async_receive_output(self): + return self.detect_api.receive_output() - def receiveSignal(signalNumber, frame): - stop_event.set() - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) +class DetectorRunner(FrigateProcess): + def __init__( + self, + name, + detection_queue: Queue, + cameras: list[str], + avg_speed: Value, + start_time: Value, + 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 = {} - frame_manager = SharedMemoryFrameManager() - object_detector = LocalObjectDetector(detector_config=detector_config) - - outputs = {} - for name in out_events.keys(): + def create_output_shm(self, name: str): out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) - outputs[name] = {"shm": out_shm, "np": out_np} + self.outputs[name] = {"shm": out_shm, "np": out_np} - while not stop_event.is_set(): - try: - connection_id = detection_queue.get(timeout=1) - except queue.Empty: - continue - input_frame = frame_manager.get( - connection_id, - (1, detector_config.model.height, detector_config.model.width, 3), + 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, + self.detector_config.model.width, + 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() + detections = object_detector.detect_raw(input_frame) + duration = datetime.datetime.now().timestamp() - self.start_time.value + 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, + detection_queue: Queue, + cameras: list[str], + avg_speed: Value, + start_time: Value, + 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 = {} + self._frame_manager: SharedMemoryFrameManager | None = None + self._publisher: ObjectDetectorPublisher | None = None + self._detector: AsyncLocalObjectDetector | None = None + self.send_times = deque() + + def create_output_shm(self, name: str): + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np = 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 + + input_frame = self._frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, + self.detector_config.model.width, + 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()) + 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(): + 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 + 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[:] + 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 ) - if input_frame is None: - logger.warning(f"Failed to get frame {connection_id} from SHM") - continue + for name in self.cameras: + self.create_output_shm(name) - # detect and send the output - start.value = datetime.datetime.now().timestamp() - detections = object_detector.detect_raw(input_frame) - duration = datetime.datetime.now().timestamp() - start.value - frame_manager.close(connection_id) - outputs[connection_id]["np"][:] = detections[:] - out_events[connection_id].set() - start.value = 0.0 + 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() - avg_speed.value = (avg_speed.value * 9 + duration) / 10 + try: + while not self.stop_event.is_set(): + time.sleep(0.5) - logger.info("Exited detection process...") + 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: @@ -150,23 +314,27 @@ class ObjectDetectProcess: self, name: str, detection_queue: Queue, - out_events: dict[str, MpEvent], + cameras: list[str], + config: FrigateConfig, detector_config: BaseDetectorConfig, + stop_event: MpEvent, ): self.name = name - self.out_events = out_events + 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: util.Process | None = None + 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): # if the process has already exited on its own, just return if self.detect_process and self.detect_process.exitcode: return - self.detect_process.terminate() + logging.info("Waiting for detection process to exit gracefully...") self.detect_process.join(timeout=30) if self.detect_process.exitcode is None: @@ -179,19 +347,30 @@ class ObjectDetectProcess: self.detection_start.value = 0.0 if (self.detect_process is not None) and self.detect_process.is_alive(): self.stop() - self.detect_process = util.Process( - target=run_detector, - name=f"detector:{self.name}", - args=( - self.name, + + # Async path for MemryX + if self.detector_config.type == "memryx": + self.detect_process = AsyncDetectorRunner( + f"frigate.detector:{self.name}", self.detection_queue, - self.out_events, + self.cameras, self.avg_inference_speed, self.detection_start, + self.config, self.detector_config, - ), - ) - self.detect_process.daemon = True + 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() @@ -201,7 +380,6 @@ class RemoteObjectDetector: name: str, labels: dict[int, str], detection_queue: Queue, - event: MpEvent, model_config: ModelConfig, stop_event: MpEvent, ): @@ -209,7 +387,6 @@ class RemoteObjectDetector: self.name = name self.fps = EventsPerSecond() self.detection_queue = detection_queue - self.event = event self.stop_event = stop_event self.shm = UntrackedSharedMemory(name=self.name, create=False) self.np_shm = np.ndarray( @@ -219,6 +396,7 @@ class RemoteObjectDetector: ) self.out_shm = UntrackedSharedMemory(name=f"out-{self.name}", create=False) self.out_np_shm = np.ndarray((20, 6), dtype=np.float32, buffer=self.out_shm.buf) + self.detector_subscriber = ObjectDetectorSubscriber(name) def detect(self, tensor_input, threshold=0.4): detections = [] @@ -226,11 +404,19 @@ class RemoteObjectDetector: 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.event.clear() self.detection_queue.put(self.name) - result = self.event.wait(timeout=5.0) + result = self.detector_subscriber.check_for_update() # if it timed out if result is None: @@ -246,5 +432,6 @@ class RemoteObjectDetector: return detections def cleanup(self): + self.detector_subscriber.stop() self.shm.unlink() self.out_shm.unlink() diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index b295af82e..eb23c2573 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -9,15 +9,16 @@ import os import queue import subprocess as sp import threading +import time import traceback from typing import Any, Optional import cv2 import numpy as np -from frigate.comms.config_updater import ConfigSubscriber +from frigate.comms.inter_process import InterProcessRequestor from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig -from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR +from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT from frigate.util.image import ( SharedMemoryFrameManager, copy_yuv_to_position, @@ -319,35 +320,48 @@ class BirdsEyeFrameManager: self.frame[:] = self.blank_frame self.cameras = {} - for camera, settings in self.config.cameras.items(): - # 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[camera] = { - "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, - }, - } + for camera in self.config.cameras.keys(): + self.add_camera(camera) self.camera_layout = [] self.active_cameras = set() self.last_output_time = 0.0 + def add_camera(self, cam: str): + """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): + """Remove a camera from self.cameras.""" + if cam in self.cameras: + del self.cameras[cam] + def clear_frame(self): logger.debug("Clearing the birdseye frame") self.frame[:] = self.blank_frame @@ -381,10 +395,24 @@ class BirdsEyeFrameManager: if mode == BirdseyeModeEnum.objects and object_box_count > 0: return True - def update_frame(self, frame: Optional[np.ndarray] = None) -> bool: + 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: Optional[np.ndarray] = None) -> tuple[bool, bool]: """ Update birdseye, optionally with a new frame. - When no frame is passed, check the layout and update for any disabled cameras. + 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 @@ -422,19 +450,21 @@ class BirdsEyeFrameManager: max_camera_refresh = True self.last_refresh_time = now - # Track if the frame changes + # 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 + 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: @@ -454,7 +484,7 @@ class BirdsEyeFrameManager: 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( @@ -504,7 +534,7 @@ class BirdsEyeFrameManager: # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas while calculating: if self.stop_event.is_set(): - return + return frame_changed, layout_changed layout_candidate = self.calculate_layout( active_cameras_to_add, coefficient @@ -518,7 +548,7 @@ class BirdsEyeFrameManager: logger.error( "Error finding appropriate birdseye layout" ) - return + return frame_changed, layout_changed calculating = False self.canvas.set_coefficient(len(active_cameras), coefficient) @@ -536,7 +566,7 @@ class BirdsEyeFrameManager: if frame is not None: # Frame presence indicates a potential change frame_changed = True - return frame_changed + return frame_changed, layout_changed def calculate_layout( self, @@ -688,7 +718,11 @@ class BirdsEyeFrameManager: motion_count: int, frame_time: float, frame: np.ndarray, - ) -> bool: + ) -> 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 birdseye is disabled for this camera camera_config = self.config.cameras[camera] force_update = False @@ -701,7 +735,7 @@ class BirdsEyeFrameManager: self.cameras[camera]["last_active_frame"] = 0 force_update = True else: - return False + return False, False # update the last active frame for the camera self.cameras[camera]["current_frame"] = frame.copy() @@ -713,21 +747,22 @@ class BirdsEyeFrameManager: # limit output to 10 fps if not force_update and (now - self.last_output_time) < 1 / 10: - return False + return False, False try: - updated_frame = self.update_frame(frame) + frame_changed, layout_changed = self.update_frame(frame) except Exception: - updated_frame = False + frame_changed, layout_changed = False, False self.active_cameras = [] self.camera_layout = [] print(traceback.format_exc()) # if the frame was updated or the fps is too low, send frame - if force_update or updated_frame or (now - self.last_output_time) > 1: + if force_update or frame_changed or (now - self.last_output_time) > 1: self.last_output_time = now - return True - return False + return True, layout_changed + + return False, layout_changed class Birdseye: @@ -753,10 +788,14 @@ class Birdseye: self.broadcaster = BroadcastThread( "birdseye", self.converter, websocket_server, stop_event ) - self.birdseye_manager = BirdsEyeFrameManager(config, stop_event) - self.birdseye_subscriber = ConfigSubscriber("config/birdseye/") + 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: Optional[float] = ( + (1.0 / self.idle_fps) if self.idle_fps > 0 else None + ) if config.birdseye.restream: self.birdseye_buffer = self.frame_manager.create( @@ -783,6 +822,16 @@ class Birdseye: 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, @@ -791,30 +840,29 @@ class Birdseye: frame_time: float, frame: np.ndarray, ) -> None: - # check if there is an updated config - while True: - ( - updated_birdseye_topic, - updated_birdseye_config, - ) = self.birdseye_subscriber.check_for_update() - - if not updated_birdseye_topic: - break - - if updated_birdseye_config: - camera_name = updated_birdseye_topic.rpartition("/")[-1] - self.config.cameras[camera_name].birdseye = updated_birdseye_config - - if self.birdseye_manager.update( + 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 = time.monotonic() + 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.birdseye_subscriber.stop() self.converter.join() self.broadcaster.join() diff --git a/frigate/output/output.py b/frigate/output/output.py index 1723ac73c..a44415000 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -2,14 +2,12 @@ import datetime import logging -import multiprocessing as mp import os import shutil -import signal import threading +from multiprocessing.synchronize import Event as MpEvent from wsgiref.simple_server import make_server -from setproctitle import setproctitle from ws4py.server.wsgirefserver import ( WebSocketWSGIHandler, WebSocketWSGIRequestHandler, @@ -17,15 +15,19 @@ from ws4py.server.wsgirefserver import ( ) 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.const import CACHE_DIR, CLIPS_DIR +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import CACHE_DIR, CLIPS_DIR, PROCESS_PRIORITY_MED from frigate.output.birdseye import Birdseye from frigate.output.camera import JsmpegCamera from frigate.output.preview import PreviewRecorder from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame +from frigate.util.process import FrigateProcess logger = logging.getLogger(__name__) @@ -70,183 +72,203 @@ def check_disabled_camera_update( birdseye.all_cameras_disabled() -def output_frames( - config: FrigateConfig, -): - threading.current_thread().name = "output" - setproctitle("frigate.output") +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 - stop_event = mp.Event() + def run(self) -> None: + self.pre_run_setup(self.config.logger) - def receiveSignal(signalNumber, frame): - stop_event.set() + frame_manager = SharedMemoryFrameManager() - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) + # 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) - frame_manager = SharedMemoryFrameManager() + detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.birdseye, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.record, + ], + ) - # 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) + 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() - detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video) - config_enabled_subscriber = ConfigSubscriber("config/enabled/") + move_preview_frames("cache") - 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() + for camera, cam_config in self.config.cameras.items(): + if not cam_config.enabled_in_config: + continue - move_preview_frames("cache") - - for camera, cam_config in config.cameras.items(): - if not cam_config.enabled_in_config: - continue - - jsmpeg_cameras[camera] = JsmpegCamera(cam_config, stop_event, websocket_server) - preview_recorders[camera] = PreviewRecorder(cam_config) - preview_write_times[camera] = 0 - - if config.birdseye.enabled: - birdseye = Birdseye(config, stop_event, websocket_server) - - websocket_thread.start() - - while not stop_event.is_set(): - # check if there is an updated config - while True: - ( - updated_enabled_topic, - updated_enabled_config, - ) = config_enabled_subscriber.check_for_update() - - if not updated_enabled_topic: - break - - if updated_enabled_config: - camera_name = updated_enabled_topic.rpartition("/")[-1] - config.cameras[camera_name].enabled = updated_enabled_config.enabled - - (topic, data) = detection_subscriber.check_for_update(timeout=1) - 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( - config, birdseye, preview_recorders, preview_write_times + jsmpeg_cameras[camera] = JsmpegCamera( + cam_config, self.stop_event, websocket_server ) + preview_recorders[camera] = PreviewRecorder(cam_config) + preview_write_times[camera] = 0 - if not topic: - continue + if self.config.birdseye.enabled: + birdseye = Birdseye(self.config, self.stop_event, websocket_server) - ( - camera, - frame_name, - frame_time, - current_tracked_objects, - motion_boxes, - _, - ) = data + websocket_thread.start() - if not config.cameras[camera].enabled: - continue + while not self.stop_event.is_set(): + # check if there is an updated config + updates = config_subscriber.check_for_updates() - frame = frame_manager.get(frame_name, config.cameras[camera].frame_shape_yuv) + if CameraConfigUpdateEnum.add in updates: + for camera in updates["add"]: + jsmpeg_cameras[camera] = JsmpegCamera( + self.config.cameras[camera], self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder( + self.config.cameras[camera] + ) + preview_write_times[camera] = 0 - 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 ( + self.config.birdseye.enabled + and self.config.cameras[camera].birdseye.enabled + ): + birdseye.add_camera(camera) - if failed_frame_requests[camera] > config.cameras[camera].detect.fps: - logger.warning( - f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues." + (topic, data) = detection_subscriber.check_for_update(timeout=1) + 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 ) - continue - else: - failed_frame_requests[camera] = 0 + if not topic: + continue - # 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) 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 config.birdseye.enabled and ( - config.birdseye.restream - or any( - ws.environ["PATH_INFO"].endswith("birdseye") - for ws in websocket_server.manager - ) - ): - birdseye.write_data( + ( camera, + frame_name, + frame_time, current_tracked_objects, motion_boxes, - frame_time, - frame, + _, + ) = data + + if not self.config.cameras[camera].enabled: + continue + + frame = frame_manager.get( + frame_name, self.config.cameras[camera].frame_shape_yuv ) - frame_manager.close(frame_name) + 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 - move_preview_frames("clips") + 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." + ) - while True: - (topic, data) = detection_subscriber.check_for_update(timeout=0) + continue + else: + failed_frame_requests[camera] = 0 - if not topic: - break + # 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 - ( - camera, - frame_name, - frame_time, - current_tracked_objects, - motion_boxes, - regions, - ) = data + # send camera frame to ffmpeg process if websockets are connected + if any( + ws.environ["PATH_INFO"].endswith(camera) + 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()) - frame = frame_manager.get(frame_name, config.cameras[camera].frame_shape_yuv) - frame_manager.close(frame_name) + # send output data to birdseye if websocket is connected or restreaming + if self.config.birdseye.enabled and ( + self.config.birdseye.restream + or any( + ws.environ["PATH_INFO"].endswith("birdseye") + for ws in websocket_server.manager + ) + ): + birdseye.write_data( + camera, + current_tracked_objects, + motion_boxes, + frame_time, + frame, + ) - detection_subscriber.stop() + frame_manager.close(frame_name) - for jsmpeg in jsmpeg_cameras.values(): - jsmpeg.stop() + move_preview_frames("clips") - for preview in preview_recorders.values(): - preview.stop() + while True: + (topic, data) = detection_subscriber.check_for_update(timeout=0) - if birdseye is not None: - birdseye.stop() + if not topic: + break - config_enabled_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...") + ( + 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() + 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): diff --git a/frigate/output/preview.py b/frigate/output/preview.py index 08caa6738..6dfd90904 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -13,7 +13,6 @@ from typing import Any import cv2 import numpy as np -from frigate.comms.config_updater import ConfigSubscriber 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 @@ -174,9 +173,6 @@ class PreviewRecorder: # create communication for finished previews self.requestor = InterProcessRequestor() - self.config_subscriber = ConfigSubscriber( - f"config/record/{self.config.name}", True - ) y, u1, u2, v1, v2 = get_yuv_crop( self.config.frame_shape_yuv, @@ -323,12 +319,6 @@ class PreviewRecorder: ) -> None: self.offline = False - # check for updated record config - _, updated_record_config = self.config_subscriber.check_for_update() - - if updated_record_config: - self.config.record = updated_record_config - # always write the first frame if self.start_time == 0: self.start_time = frame_time diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index c6d43bbba..6e86ecbf2 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -31,7 +31,7 @@ from frigate.const import ( ) from frigate.ptz.onvif import OnvifController from frigate.track.tracked_object import TrackedObject -from frigate.util.builtin import update_yaml_file +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 @@ -60,10 +60,10 @@ class PtzMotionEstimator: def motion_estimator( self, - detections: list[dict[str, Any]], + detections: list[tuple[Any, Any, Any, Any, Any, Any]], frame_name: str, frame_time: float, - camera: str, + 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(): @@ -348,10 +348,13 @@ class PtzAutoTracker: f"{camera}: Writing new config with autotracker motion coefficients: {self.config.cameras[camera].onvif.autotracking.movement_weights}" ) - update_yaml_file( + update_yaml_file_bulk( config_file, - ["cameras", camera, "onvif", "autotracking", "movement_weights"], - self.config.cameras[camera].onvif.autotracking.movement_weights, + { + f"cameras.{camera}.onvif.autotracking.movement_weights": self.config.cameras[ + camera + ].onvif.autotracking.movement_weights + }, ) async def _calibrate_camera(self, camera): diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 424c4c0dd..488dbd278 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -33,6 +33,8 @@ class OnvifCommandEnum(str, Enum): stop = "stop" zoom_in = "zoom_in" zoom_out = "zoom_out" + focus_in = "focus_in" + focus_out = "focus_out" class OnvifController: @@ -93,12 +95,21 @@ class OnvifController: cam = self.camera_configs[cam_name] try: + user = cam.onvif.user + password = cam.onvif.password + + if user is not None and isinstance(user, bytes): + user = user.decode("utf-8") + + if password is not None and isinstance(password, bytes): + password = password.decode("utf-8") + self.cams[cam_name] = { "onvif": ONVIFCamera( cam.onvif.host, cam.onvif.port, - cam.onvif.user, - cam.onvif.password, + user, + password, wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"), adjust_time=cam.onvif.ignore_time_mismatch, encrypt=not cam.onvif.tls_insecure, @@ -188,6 +199,20 @@ class OnvifController: 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 @@ -309,9 +334,15 @@ class OnvifController: presets = [] for preset in presets: - self.cams[camera_name]["presets"][ - (getattr(preset, "Name") or f"preset {preset['token']}").lower() - ] = preset["token"] + # Ensure preset name is a Unicode string and handle UTF-8 characters correctly + preset_name = getattr(preset, "Name") or f"preset {preset['token']}" + + if isinstance(preset_name, bytes): + preset_name = preset_name.decode("utf-8") + + # Convert to lowercase while preserving UTF-8 characters + preset_name_lower = preset_name.lower() + self.cams[camera_name]["presets"][preset_name_lower] = preset["token"] # get list of supported features supported_features = [] @@ -369,7 +400,22 @@ class OnvifController: f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}" ) - # set relative pan/tilt space for autotracker + 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}") + if ( self.config.cameras[camera_name].onvif.autotracking.enabled_in_config and self.config.cameras[camera_name].onvif.autotracking.enabled @@ -394,6 +440,19 @@ class OnvifController: "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: @@ -519,6 +578,11 @@ class OnvifController: self.cams[camera_name]["active"] = False async def _move_to_preset(self, camera_name: str, preset: str) -> None: + if isinstance(preset, bytes): + preset = preset.decode("utf-8") + + 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 @@ -602,6 +666,36 @@ class OnvifController: 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: @@ -625,11 +719,10 @@ class OnvifController: elif command == OnvifCommandEnum.move_relative: _, pan, tilt = param.split("_") await self._move_relative(camera_name, float(pan), float(tilt), 0, 1) - elif ( - command == OnvifCommandEnum.zoom_in - or command == OnvifCommandEnum.zoom_out - ): + 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: @@ -640,7 +733,6 @@ class OnvifController: ) -> None: """ Handle ONVIF commands by scheduling them in the event loop. - This is the synchronous interface that schedules async work. """ future = asyncio.run_coroutine_threadsafe( self.handle_command_async(camera_name, command, param), self.loop diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 1de08a899..94dd43eba 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -14,7 +14,8 @@ 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.record.util import remove_empty_directories, sync_recordings -from frigate.util.builtin import clear_and_unlink, get_tomorrow_at_time +from frigate.util.builtin import clear_and_unlink +from frigate.util.time import get_tomorrow_at_time logger = logging.getLogger(__name__) @@ -100,7 +101,11 @@ class RecordingCleanup(threading.Thread): ).execute() def expire_existing_camera_recordings( - self, expire_date: float, config: CameraConfig, reviews: ReviewSegment + self, + continuous_expire_date: float, + motion_expire_date: float, + config: CameraConfig, + reviews: ReviewSegment, ) -> None: """Delete recordings for existing camera based on retention config.""" # Get the timestamp for cutoff of retained days @@ -114,10 +119,18 @@ class RecordingCleanup(threading.Thread): Recordings.path, Recordings.objects, Recordings.motion, + Recordings.dBFS, ) .where( - Recordings.camera == config.name, - Recordings.end_time < expire_date, + (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() @@ -170,7 +183,12 @@ class RecordingCleanup(threading.Thread): # Delete recordings outside of the retention window or based on the retention mode if ( not keep - or (mode == RetainModeEnum.motion and recording.motion == 0) + 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) ): Path(recording.path).unlink(missing_ok=True) @@ -188,7 +206,7 @@ class RecordingCleanup(threading.Thread): Recordings.id << deleted_recordings_list[i : i + max_deletes] ).execute() - previews: Previews = ( + previews: list[Previews] = ( Previews.select( Previews.id, Previews.start_time, @@ -196,8 +214,9 @@ class RecordingCleanup(threading.Thread): Previews.path, ) .where( - Previews.camera == config.name, - Previews.end_time < expire_date, + (Previews.camera == config.name) + & (Previews.end_time < continuous_expire_date) + & (Previews.end_time < motion_expire_date) ) .order_by(Previews.start_time) .namedtuples() @@ -253,7 +272,9 @@ class RecordingCleanup(threading.Thread): logger.debug("Start deleted cameras.") # Handle deleted cameras - expire_days = self.config.record.retain.days + expire_days = max( + self.config.record.continuous.days, self.config.record.motion.days + ) expire_before = ( datetime.datetime.now() - datetime.timedelta(days=expire_days) ).timestamp() @@ -291,9 +312,17 @@ class RecordingCleanup(threading.Thread): now = datetime.datetime.now() self.expire_review_segments(config, now) - - expire_days = config.record.retain.days - expire_date = (now - datetime.timedelta(days=expire_days)).timestamp() + 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 = ( @@ -306,13 +335,15 @@ class RecordingCleanup(threading.Thread): ReviewSegment.camera == camera, # need to ensure segments for all reviews starting # before the expire date are included - ReviewSegment.start_time < expire_date, + ReviewSegment.start_time < motion_expire_date, ) .order_by(ReviewSegment.start_time) .namedtuples() ) - self.expire_existing_camera_recordings(expire_date, config, reviews) + self.expire_existing_camera_recordings( + continuous_expire_date, motion_expire_date, config, reviews + ) logger.debug(f"End camera: {camera}.") logger.debug("End all cameras.") diff --git a/frigate/record/export.py b/frigate/record/export.py index 0d3f96da0..d4b49bb4b 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -21,13 +21,14 @@ from frigate.const import ( EXPORT_DIR, MAX_PLAYLIST_SECONDS, PREVIEW_FRAME_TYPE, + PROCESS_PRIORITY_LOW, ) from frigate.ffmpeg_presets import ( EncodeTypeEnum, parse_preset_hardware_acceleration_encode, ) from frigate.models import Export, Previews, Recordings -from frigate.util.builtin import is_current_hour +from frigate.util.time import is_current_hour logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" def lower_priority(): - os.nice(10) + os.nice(PROCESS_PRIORITY_LOW) class PlaybackFactorEnum(str, Enum): diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index f1b9a600e..25c9d2cff 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -16,7 +16,6 @@ from typing import Any, Optional, Tuple import numpy as np import psutil -from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.recordings_updater import ( @@ -24,6 +23,10 @@ from frigate.comms.recordings_updater import ( RecordingsDataTypeEnum, ) from frigate.config import FrigateConfig, RetainModeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) from frigate.const import ( CACHE_DIR, CACHE_SEGMENT_FORMAT, @@ -54,14 +57,25 @@ class SegmentInfo: self.average_dBFS = average_dBFS def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool: - return ( - retain_mode == RetainModeEnum.motion - and self.motion_count == 0 - and self.average_dBFS == 0 - ) or ( - retain_mode == RetainModeEnum.active_objects - and self.active_object_count == 0 - ) + 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): @@ -71,16 +85,19 @@ class RecordingMaintainer(threading.Thread): # create communication for retained recordings self.requestor = InterProcessRequestor() - self.config_subscriber = ConfigSubscriber("config/record/") - self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) - self.recordings_publisher = RecordingsDataPublisher( - RecordingsDataTypeEnum.recordings_available_through + 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 = [ @@ -91,6 +108,48 @@ class RecordingMaintainer(threading.Thread): 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.timezone.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: @@ -104,7 +163,7 @@ class RecordingMaintainer(threading.Thread): except psutil.Error: continue - # group recordings by camera + # 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 @@ -113,7 +172,13 @@ class RecordingMaintainer(threading.Thread): cache_path = os.path.join(CACHE_DIR, cache) basename = os.path.splitext(cache)[0] - camera, date = basename.rsplit("@", maxsplit=1) + 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( @@ -226,7 +291,9 @@ class RecordingMaintainer(threading.Thread): recordings[0]["start_time"].timestamp() if self.config.cameras[camera].record.enabled else None, - ) + None, + ), + RecordingsDataTypeEnum.saved.value, ) recordings_to_insert: list[Optional[Recordings]] = await asyncio.gather(*tasks) @@ -243,7 +310,7 @@ class RecordingMaintainer(threading.Thread): async def validate_and_move_segment( self, camera: str, reviews: list[ReviewSegment], recording: dict[str, Any] - ) -> None: + ) -> Optional[Recordings]: cache_path: str = recording["cache_path"] start_time: datetime.datetime = recording["start_time"] record_config = self.config.cameras[camera].record @@ -254,7 +321,7 @@ class RecordingMaintainer(threading.Thread): or not self.config.cameras[camera].record.enabled ): self.drop_segment(cache_path) - return + return None if cache_path in self.end_time_cache: end_time, duration = self.end_time_cache[cache_path] @@ -263,10 +330,18 @@ class RecordingMaintainer(threading.Thread): self.config.ffmpeg, cache_path, get_duration=True ) - if segment_info["duration"]: - duration = float(segment_info["duration"]) - else: - duration = -1 + 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: @@ -277,71 +352,31 @@ class RecordingMaintainer(threading.Thread): logger.warning(f"Failed to probe corrupt segment {cache_path}") logger.warning(f"Discarding a corrupt recording segment: {cache_path}") - Path(cache_path).unlink(missing_ok=True) - return - - # if cached file's start_time is earlier than the retain days for the camera - # meaning continuous recording is not enabled - if start_time <= ( - datetime.datetime.now().astimezone(datetime.timezone.utc) - - datetime.timedelta(days=self.config.cameras[camera].record.retain.days) - ): - # 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 + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.invalid.value, ) - # move from cache to recordings immediately - return await self.move_segment( - camera, - start_time, - end_time, - duration, - cache_path, - record_mode, - ) - # if it doesn't overlap with an review item, go ahead and drop the segment - # if it ends more than the configured pre_capture for the camera - 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.timezone.utc) - if end_time < retain_cutoff: - self.drop_segment(cache_path) - # else retain days includes this segment - # meaning continuous recording is enabled - else: + 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 + 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 = ( @@ -355,11 +390,68 @@ class RecordingMaintainer(threading.Thread): ).astimezone(datetime.timezone.utc) >= end_time ): - record_mode = self.config.cameras[camera].record.retain.mode + record_mode = ( + RetainModeEnum.all + if highest == "continuous" + else RetainModeEnum.motion + ) return await self.move_segment( camera, start_time, end_time, duration, cache_path, record_mode ) + # 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 + ) + # move from cache to recordings immediately + return await self.move_segment( + camera, + start_time, + end_time, + duration, + cache_path, + record_mode, + ) + # if it doesn't overlap with an review item, go ahead and drop the segment + # if it ends more than the configured pre_capture for the camera + # BUT only if continuous/motion is NOT enabled (otherwise wait for processing) + elif highest is None: + 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.timezone.utc) + if end_time < retain_cutoff: + self.drop_segment(cache_path) + def segment_stats( self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime ) -> SegmentInfo: @@ -518,17 +610,7 @@ class RecordingMaintainer(threading.Thread): run_start = datetime.datetime.now().timestamp() # check if there is an updated config - while True: - ( - updated_topic, - updated_record_config, - ) = self.config_subscriber.check_for_update() - - if not updated_topic: - break - - camera_name = updated_topic.rpartition("/")[-1] - self.config.cameras[camera_name].record = updated_record_config + self.config_subscriber.check_for_updates() stale_frame_count = 0 stale_frame_count_threshold = 10 @@ -541,7 +623,7 @@ class RecordingMaintainer(threading.Thread): if not topic: break - if topic == DetectionTypeEnum.video: + if topic == DetectionTypeEnum.video.value: ( camera, _, @@ -560,7 +642,7 @@ class RecordingMaintainer(threading.Thread): regions, ) ) - elif topic == DetectionTypeEnum.audio: + elif topic == DetectionTypeEnum.audio.value: ( camera, frame_time, @@ -576,7 +658,9 @@ class RecordingMaintainer(threading.Thread): audio_detections, ) ) - elif topic == DetectionTypeEnum.api or DetectionTypeEnum.lpr: + elif ( + topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value + ): continue if frame_time < run_start - stale_frame_count_threshold: diff --git a/frigate/record/record.py b/frigate/record/record.py index 252b80545..624ed6e9a 100644 --- a/frigate/record/record.py +++ b/frigate/record/record.py @@ -1,50 +1,47 @@ """Run recording maintainer and cleanup.""" import logging -import multiprocessing as mp -import signal -import threading -from types import FrameType -from typing import Optional +from multiprocessing.synchronize import Event as MpEvent from playhouse.sqliteq import SqliteQueueDatabase -from setproctitle import setproctitle 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.services import listen +from frigate.util.process import FrigateProcess logger = logging.getLogger(__name__) -def manage_recordings(config: FrigateConfig) -> None: - stop_event = mp.Event() +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 receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: - stop_event.set() + 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) - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) - - threading.current_thread().name = "process:recording_manager" - setproctitle("frigate.recording_manager") - listen() - - db = SqliteQueueDatabase( - 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])), - ) - models = [ReviewSegment, Recordings] - db.bind(models) - - maintainer = RecordingMaintainer( - config, - stop_event, - ) - maintainer.start() + maintainer = RecordingMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index b144b6e52..917c0c5ac 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -1,6 +1,7 @@ """Maintain review segments in db.""" import copy +import datetime import json import logging import os @@ -15,10 +16,14 @@ from typing import Any, Optional import cv2 import numpy as np -from frigate.comms.config_updater import ConfigSubscriber 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, @@ -35,9 +40,6 @@ logger = logging.getLogger(__name__) THUMB_HEIGHT = 180 THUMB_WIDTH = 320 -THRESHOLD_ALERT_ACTIVITY = 120 -THRESHOLD_DETECTION_ACTIVITY = 30 - class PendingReviewSegment: def __init__( @@ -59,7 +61,12 @@ class PendingReviewSegment: self.sub_labels = sub_labels self.zones = zones self.audio = audio - self.last_update = frame_time + 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.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8) @@ -101,6 +108,7 @@ class PendingReviewSegment: ) 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] @@ -120,25 +128,134 @@ class PendingReviewSegment: ) 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: self.last_update if ended else None, + 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[TrackedObject], + ): + self.camera_config = camera_config + + # get current categorization of objects to know if + # these objects are currently being categorized + self.categorized_objects = { + "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[TrackedObject]: + return ( + self.categorized_objects["alerts"] + self.categorized_objects["detections"] + ) + + class ReviewSegmentMaintainer(threading.Thread): """Maintain review segments.""" @@ -150,10 +267,19 @@ class ReviewSegmentMaintainer(threading.Thread): # create communication for review segments self.requestor = InterProcessRequestor() - self.record_config_subscriber = ConfigSubscriber("config/record/") - self.review_config_subscriber = ConfigSubscriber("config/review/") - self.enabled_config_subscriber = ConfigSubscriber("config/enabled/") - self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) + 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]] = {} @@ -174,16 +300,16 @@ class ReviewSegmentMaintainer(threading.Thread): 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( - { - "type": "new", - "before": start_data, - "after": start_data, - } - ), + json.dumps(review_update), ) + self.review_publisher.publish(review_update, segment.camera) self.requestor.send_data( f"{segment.camera}/review_status", segment.severity.value.upper() ) @@ -202,16 +328,16 @@ class ReviewSegmentMaintainer(threading.Thread): 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( - { - "type": "update", - "before": {k: v for k, v in prev_data.items()}, - "after": {k: v for k, v in new_data.items()}, - } - ), + json.dumps(review_update), ) + self.review_publisher.publish(review_update, segment.camera) self.requestor.send_data( f"{segment.camera}/review_status", segment.severity.value.upper() ) @@ -220,29 +346,31 @@ class ReviewSegmentMaintainer(threading.Thread): self, segment: PendingReviewSegment, prev_data: dict[str, Any], - ) -> None: + ) -> float: """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( - { - "type": "end", - "before": {k: v for k, v in prev_data.items()}, - "after": {k: v for k, v in final_data.items()}, - } - ), + json.dumps(review_update), ) + self.review_publisher.publish(review_update, segment.camera) self.requestor.send_data(f"{segment.camera}/review_status", "NONE") self.active_review_segments[segment.camera] = None + return end_time - def end_segment(self, camera: str) -> None: - """End the pending segment for a camera.""" + def forcibly_end_segment(self, camera: str) -> float: + """Forcibly end the pending segment for a camera.""" segment = self.active_review_segments.get(camera) if segment: prev_data = segment.get_data(False) - self._publish_segment_end(segment, prev_data) + return self._publish_segment_end(segment, prev_data) def update_existing_segment( self, @@ -255,21 +383,43 @@ class ReviewSegmentMaintainer(threading.Thread): camera_config = self.config.cameras[segment.camera] # get active objects + objects loitering in loitering zones - active_objects = get_active_objects( - frame_time, camera_config, objects - ) + get_loitering_objects(frame_time, camera_config, objects) + activity = ActiveObjects(frame_time, camera_config, objects) prev_data = segment.get_data(False) has_activity = False - if len(active_objects) > 0: + if activity.has_active_objects(): has_activity = True should_update_image = False should_update_state = False - if frame_time > segment.last_update: - segment.last_update = frame_time + if activity.has_activity_category(SeverityEnum.alert): + # update current time for last alert activity + 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): + 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 - for object in active_objects: if not object["sub_label"]: segment.detections[object["id"]] = object["label"] elif object["sub_label"][0] in self.config.model.all_attributes: @@ -278,33 +428,13 @@ class ReviewSegmentMaintainer(threading.Thread): segment.detections[object["id"]] = f"{object['label']}-verified" segment.sub_labels[object["id"]] = object["sub_label"][0] - # if object is alert label - # and has entered required zones or required zones is not set - # mark this review as alert - if ( - segment.severity != SeverityEnum.alert - and object["label"] in camera_config.review.alerts.labels - and ( - not camera_config.review.alerts.required_zones - or ( - len(object["current_zones"]) > 0 - and set(object["current_zones"]) - & set(camera_config.review.alerts.required_zones) - ) - ) - and camera_config.review.alerts.enabled - ): - segment.severity = SeverityEnum.alert - should_update_state = True - should_update_image = True - # 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(active_objects) > segment.frame_active_count: + if len(activity.get_all_objects()) > segment.frame_active_count: should_update_state = True should_update_image = True @@ -325,7 +455,11 @@ class ReviewSegmentMaintainer(threading.Thread): yuv_frame = None self._publish_segment_update( - segment, camera_config, yuv_frame, active_objects, prev_data + segment, + camera_config, + yuv_frame, + activity.get_all_objects(), + prev_data, ) self.frame_manager.close(frame_name) except FileNotFoundError: @@ -351,10 +485,50 @@ class ReviewSegmentMaintainer(threading.Thread): return if segment.severity == SeverityEnum.alert and frame_time > ( - segment.last_update + THRESHOLD_ALERT_ACTIVITY + 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: + self.active_review_segments[activity.camera_config.name] = ( + PendingReviewSegment( + activity.camera_config.name, + end_time, + SeverityEnum.detection, + new_detections, + sub_labels={}, + audio=set(), + zones=list(new_zones), + ) + ) + self._publish_segment_start( + self.active_review_segments[activity.camera_config.name] + ) + self.active_review_segments[ + activity.camera_config.name + ].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) - elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY): self._publish_segment_end(segment, prev_data) def check_if_new_segment( @@ -366,15 +540,26 @@ class ReviewSegmentMaintainer(threading.Thread): ) -> None: """Check if a new review segment should be created.""" camera_config = self.config.cameras[camera] - active_objects = get_active_objects(frame_time, camera_config, objects) + activity = ActiveObjects(frame_time, camera_config, objects) - if len(active_objects) > 0: + if activity.has_active_objects(): detections: dict[str, str] = {} sub_labels: dict[str, str] = {} zones: list[str] = [] - severity = None + severity: SeverityEnum | None = None - for object in active_objects: + # 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: @@ -383,46 +568,6 @@ class ReviewSegmentMaintainer(threading.Thread): detections[object["id"]] = f"{object['label']}-verified" sub_labels[object["id"]] = object["sub_label"][0] - # if object is alert label - # and has entered required zones or required zones is not set - # mark this review as alert - if ( - severity != SeverityEnum.alert - and object["label"] in camera_config.review.alerts.labels - and ( - not camera_config.review.alerts.required_zones - or ( - len(object["current_zones"]) > 0 - and set(object["current_zones"]) - & set(camera_config.review.alerts.required_zones) - ) - ) - and camera_config.review.alerts.enabled - ): - severity = SeverityEnum.alert - - # if object is detection label - # and review is not already a detection or alert - # and has entered required zones or required zones is not set - # mark this review as detection - if ( - not severity - and ( - camera_config.review.detections.labels is None - or object["label"] in (camera_config.review.detections.labels) - ) - and ( - not camera_config.review.detections.required_zones - or ( - len(object["current_zones"]) > 0 - and set(object["current_zones"]) - & set(camera_config.review.detections.required_zones) - ) - ) - and camera_config.review.detections.enabled - ): - severity = SeverityEnum.detection - for zone in object["current_zones"]: if zone not in zones: zones.append(zone) @@ -448,7 +593,7 @@ class ReviewSegmentMaintainer(threading.Thread): return self.active_review_segments[camera].update_frame( - camera_config, yuv_frame, active_objects + camera_config, yuv_frame, activity.get_all_objects() ) self.frame_manager.close(frame_name) self._publish_segment_start(self.active_review_segments[camera]) @@ -458,57 +603,22 @@ class ReviewSegmentMaintainer(threading.Thread): def run(self) -> None: while not self.stop_event.is_set(): # check if there is an updated config - while True: - ( - updated_record_topic, - updated_record_config, - ) = self.record_config_subscriber.check_for_update() + updated_topics = self.config_subscriber.check_for_updates() - ( - updated_review_topic, - updated_review_config, - ) = self.review_config_subscriber.check_for_update() + if "record" in updated_topics: + for camera in updated_topics["record"]: + self.forcibly_end_segment(camera) - ( - updated_enabled_topic, - updated_enabled_config, - ) = self.enabled_config_subscriber.check_for_update() - - if ( - not updated_record_topic - and not updated_review_topic - and not updated_enabled_topic - ): - break - - if updated_record_topic: - camera_name = updated_record_topic.rpartition("/")[-1] - self.config.cameras[camera_name].record = updated_record_config - - # immediately end segment - if not updated_record_config.enabled: - self.end_segment(camera_name) - - if updated_review_topic: - camera_name = updated_review_topic.rpartition("/")[-1] - self.config.cameras[camera_name].review = updated_review_config - - if updated_enabled_config: - camera_name = updated_enabled_topic.rpartition("/")[-1] - self.config.cameras[ - camera_name - ].enabled = updated_enabled_config.enabled - - # immediately end segment as we may not get another update - if not updated_enabled_config.enabled: - self.end_segment(camera_name) + if "enabled" in updated_topics: + for camera in updated_topics["enabled"]: + self.forcibly_end_segment(camera) (topic, data) = self.detection_subscriber.check_for_update(timeout=1) if not topic: continue - if topic == DetectionTypeEnum.video: + if topic == DetectionTypeEnum.video.value: ( camera, frame_name, @@ -517,14 +627,14 @@ class ReviewSegmentMaintainer(threading.Thread): _, _, ) = data - elif topic == DetectionTypeEnum.audio: + elif topic == DetectionTypeEnum.audio.value: ( camera, frame_time, _, audio_detections, ) = data - elif topic == DetectionTypeEnum.api or DetectionTypeEnum.lpr: + elif topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value: ( camera, frame_time, @@ -551,7 +661,7 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.severity == SeverityEnum.detection and not self.config.cameras[camera].review.detections.enabled ): - self.end_segment(camera) + self.forcibly_end_segment(camera) continue # If we reach here, the segment can be processed (if it exists) @@ -566,9 +676,6 @@ class ReviewSegmentMaintainer(threading.Thread): elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: camera_config = self.config.cameras[camera] - if frame_time > current_segment.last_update: - current_segment.last_update = frame_time - for audio in audio_detections: if ( audio in camera_config.review.alerts.labels @@ -576,11 +683,13 @@ class ReviewSegmentMaintainer(threading.Thread): ): 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"]] = ( @@ -596,7 +705,7 @@ class ReviewSegmentMaintainer(threading.Thread): and self.config.cameras[camera].review.detections.enabled ): current_segment.severity = SeverityEnum.detection - current_segment.last_update = manual_info["end_time"] + 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"] @@ -616,7 +725,8 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.severity = SeverityEnum.detection # temporarily make it so this event can not end - current_segment.last_update = sys.maxsize + 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"] @@ -624,7 +734,12 @@ class ReviewSegmentMaintainer(threading.Thread): self.indefinite_events[camera].pop(event_id) if len(self.indefinite_events[camera]) == 0: - current_segment.last_update = manual_info["end_time"] + 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." @@ -692,11 +807,17 @@ class ReviewSegmentMaintainer(threading.Thread): # temporarily make it so this event can not end self.active_review_segments[ camera - ].last_update = sys.maxsize + ].last_alert_time = sys.maxsize + self.active_review_segments[ + camera + ].last_detection_time = sys.maxsize elif manual_info["state"] == ManualEventState.complete: self.active_review_segments[ camera - ].last_update = manual_info["end_time"] + ].last_alert_time = manual_info["end_time"] + self.active_review_segments[ + camera + ].last_detection_time = manual_info["end_time"] else: logger.warning( f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert." @@ -720,61 +841,23 @@ class ReviewSegmentMaintainer(threading.Thread): # temporarily make it so this event can not end self.active_review_segments[ camera - ].last_update = sys.maxsize + ].last_alert_time = sys.maxsize + self.active_review_segments[ + camera + ].last_detection_time = sys.maxsize elif manual_info["state"] == ManualEventState.complete: self.active_review_segments[ camera - ].last_update = manual_info["end_time"] + ].last_alert_time = manual_info["end_time"] + self.active_review_segments[ + camera + ].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.record_config_subscriber.stop() - self.review_config_subscriber.stop() + self.config_subscriber.stop() self.requestor.stop() self.detection_subscriber.stop() logger.info("Exiting review maintainer...") - - -def get_active_objects( - frame_time: float, camera_config: CameraConfig, all_objects: list[TrackedObject] -) -> list[TrackedObject]: - """get active objects for detection.""" - return [ - o - for o in all_objects - if o["motionless_count"] - < camera_config.detect.stationary.threshold # no stationary objects - and o["position_changes"] > 0 # object must have moved at least once - and o["frame_time"] == frame_time # object must be detected in this frame - and not o["false_positive"] # object must not be a false positive - and ( - o["label"] in camera_config.review.alerts.labels - or ( - camera_config.review.detections.labels is None - or o["label"] in camera_config.review.detections.labels - ) - ) # object must be in the alerts or detections label list - ] - - -def get_loitering_objects( - frame_time: float, camera_config: CameraConfig, all_objects: list[TrackedObject] -) -> list[TrackedObject]: - """get loitering objects for detection.""" - return [ - o - for o in all_objects - if o["pending_loitering"] # object must be pending loitering - and o["position_changes"] > 0 # object must have moved at least once - and o["frame_time"] == frame_time # object must be detected in this frame - and not o["false_positive"] # object must not be a false positive - and ( - o["label"] in camera_config.review.alerts.labels - or ( - camera_config.review.detections.labels is None - or o["label"] in camera_config.review.detections.labels - ) - ) # object must be in the alerts or detections label list - ] diff --git a/frigate/review/review.py b/frigate/review/review.py index dafa6c802..c00c302a2 100644 --- a/frigate/review/review.py +++ b/frigate/review/review.py @@ -1,36 +1,30 @@ """Run recording maintainer and cleanup.""" import logging -import multiprocessing as mp -import signal -import threading -from types import FrameType -from typing import Optional - -from setproctitle import setproctitle +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.services import listen +from frigate.util.process import FrigateProcess logger = logging.getLogger(__name__) -def manage_review_segments(config: FrigateConfig) -> None: - stop_event = mp.Event() +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 receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: - stop_event.set() - - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) - - threading.current_thread().name = "process:review_segment_manager" - setproctitle("frigate.review_segment_manager") - listen() - - maintainer = ReviewSegmentMaintainer( - config, - stop_event, - ) - maintainer.start() + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = ReviewSegmentMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/frigate/stats/prometheus.py b/frigate/stats/prometheus.py index bc545f21d..67d8d03d8 100644 --- a/frigate/stats/prometheus.py +++ b/frigate/stats/prometheus.py @@ -1,5 +1,6 @@ import logging import re +from typing import Any, Dict, List from prometheus_client import CONTENT_TYPE_LATEST, generate_latest from prometheus_client.core import ( @@ -450,51 +451,17 @@ class CustomCollector(object): yield storage_total yield storage_used - # count events - events = [] - - if len(events) > 0: - # events[0] is newest event, last element is oldest, don't need to sort - - if not self.previous_event_id: - # ignore all previous events on startup, prometheus might have already counted them - self.previous_event_id = events[0]["id"] - self.previous_event_start_time = int(events[0]["start_time"]) - - for event in events: - # break if event already counted - if event["id"] == self.previous_event_id: - break - - # break if event starts before previous event - if event["start_time"] < self.previous_event_start_time: - break - - # store counted events in a dict - try: - cam = self.all_events[event["camera"]] - try: - cam[event["label"]] += 1 - except KeyError: - # create label dict if not exists - cam.update({event["label"]: 1}) - except KeyError: - # create camera and label dict if not exists - self.all_events.update({event["camera"]: {event["label"]: 1}}) - - # don't recount events next time - self.previous_event_id = events[0]["id"] - self.previous_event_start_time = int(events[0]["start_time"]) - camera_events = CounterMetricFamily( "frigate_camera_events", "Count of camera events since exporter started", labels=["camera", "label"], ) - for camera, cam_dict in self.all_events.items(): - for label, label_value in cam_dict.items(): - camera_events.add_metric([camera, label], label_value) + 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 @@ -503,7 +470,7 @@ collector = CustomCollector(None) REGISTRY.register(collector) -def update_metrics(stats): +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() @@ -512,6 +479,8 @@ def update_metrics(stats): # 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}") diff --git a/frigate/stats/util.py b/frigate/stats/util.py index e098bc541..410350d96 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -5,25 +5,27 @@ import os import shutil import time from json import JSONDecodeError +from multiprocessing.managers import DictProxy from typing import Any, Optional -import psutil import requests from requests.exceptions import RequestException -from frigate.camera import CameraMetrics 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_bandwidth_stats, get_cpu_stats, + get_fs_type, 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, @@ -40,11 +42,10 @@ def get_latest_version(config: FrigateConfig) -> str: "https://api.github.com/repos/blakeblackshear/frigate/releases/latest", timeout=10, ) + response = request.json() except (RequestException, JSONDecodeError): return "unknown" - response = request.json() - if request.ok and response and "tag_name" in response: return str(response.get("tag_name").replace("v", "")) else: @@ -53,7 +54,7 @@ def get_latest_version(config: FrigateConfig) -> str: def stats_init( config: FrigateConfig, - camera_metrics: dict[str, CameraMetrics], + camera_metrics: DictProxy, embeddings_metrics: DataProcessorMetrics | None, detectors: dict[str, ObjectDetectProcess], processes: dict[str, int], @@ -70,16 +71,6 @@ def stats_init( return stats_tracking -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 read_temperature(path: str) -> Optional[float]: if os.path.isfile(path): with open(path) as f: @@ -256,6 +247,10 @@ async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> No # 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 if stats: all_stats["npu_usages"] = stats @@ -268,15 +263,20 @@ def stats_snapshot( camera_metrics = stats_tracking["camera_metrics"] stats: dict[str, Any] = {} - total_detection_fps = 0 + total_camera_fps = total_process_fps = total_skipped_fps = total_detection_fps = 0 stats["cameras"] = {} for name, camera_stats in camera_metrics.items(): + 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 if camera_stats.process else None + 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 if camera_stats.capture_process else None + camera_stats.capture_process_pid.value + if camera_stats.capture_process_pid.value + else None ) stats["cameras"][name] = { "camera_fps": round(camera_stats.camera_fps.value, 2), @@ -303,6 +303,9 @@ def stats_snapshot( # from mypy 0.981 onwards "pid": pid, } + 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"] = {} @@ -354,6 +357,30 @@ def stats_snapshot( 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"] = { @@ -365,7 +392,7 @@ def stats_snapshot( "last_updated": int(time.time()), } - for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: + for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]: try: storage_stats = shutil.disk_usage(path) except (FileNotFoundError, OSError): @@ -379,6 +406,8 @@ def stats_snapshot( "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] = { diff --git a/frigate/storage.py b/frigate/storage.py index 1c4650271..feabe06ff 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -5,7 +5,7 @@ import shutil import threading from pathlib import Path -from peewee import fn +from peewee import SQL, fn from frigate.config import FrigateConfig from frigate.const import RECORD_DIR @@ -44,13 +44,19 @@ class StorageMaintainer(threading.Thread): ) } - # calculate MB/hr + # calculate MB/hr from last 100 segments try: - bandwidth = round( - Recordings.select(fn.AVG(bandwidth_equation)) + # 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) - .scalar() + .alias("recent") + ) + + bandwidth = round( + Recordings.select(fn.AVG(SQL("bw"))).from_(last_100).scalar() * 3600, 2, ) @@ -77,7 +83,10 @@ class StorageMaintainer(threading.Thread): .scalar() ) - usages[camera] = { + 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 @@ -110,6 +119,7 @@ class StorageMaintainer(threading.Thread): recordings: Recordings = ( Recordings.select( Recordings.id, + Recordings.camera, Recordings.start_time, Recordings.end_time, Recordings.segment_size, @@ -134,7 +144,7 @@ class StorageMaintainer(threading.Thread): ) event_start = 0 - deleted_recordings = set() + deleted_recordings = [] for recording in recordings: # check if 1 hour of storage has been reclaimed if deleted_segments_size > hourly_bandwidth: @@ -169,7 +179,7 @@ class StorageMaintainer(threading.Thread): if not keep: try: clear_and_unlink(Path(recording.path), missing_ok=False) - deleted_recordings.add(recording.id) + 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 @@ -183,6 +193,9 @@ class StorageMaintainer(threading.Thread): recordings = ( Recordings.select( Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, Recordings.path, Recordings.segment_size, ) @@ -198,7 +211,7 @@ class StorageMaintainer(threading.Thread): try: clear_and_unlink(Path(recording.path), missing_ok=False) deleted_segments_size += recording.segment_size - deleted_recordings.add(recording.id) + deleted_recordings.append(recording) except FileNotFoundError: # this file was not found so we must assume no space was cleaned up pass @@ -208,7 +221,50 @@ class StorageMaintainer(threading.Thread): 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) + + # 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] diff --git a/frigate/test/http_api/base_http_test.py b/frigate/test/http_api/base_http_test.py index 3c4a7ccdc..16ded63f8 100644 --- a/frigate/test/http_api/base_http_test.py +++ b/frigate/test/http_api/base_http_test.py @@ -3,6 +3,8 @@ import logging import os import unittest +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 @@ -16,6 +18,20 @@ 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 @@ -45,6 +61,9 @@ class BaseTestHttp(unittest.TestCase): }, } self.test_stats = { + "camera_fps": 5.0, + "process_fps": 5.0, + "skipped_fps": 0.0, "detection_fps": 13.7, "detectors": { "cpu1": { @@ -109,8 +128,10 @@ class BaseTestHttp(unittest.TestCase): except OSError: pass - def create_app(self, stats=None): - return create_fastapi_app( + 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, @@ -118,24 +139,56 @@ class BaseTestHttp(unittest.TestCase): None, None, stats, + event_metadata_publisher, None, + 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 = datetime.datetime.now().timestamp(), - end_time: float = datetime.datetime.now().timestamp() + 20, + 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="front_door", + camera=camera, start_time=start_time, end_time=end_time, top_score=top_score, @@ -154,15 +207,23 @@ class BaseTestHttp(unittest.TestCase): def insert_mock_review_segment( self, id: str, - start_time: float = datetime.datetime.now().timestamp(), - end_time: float = datetime.datetime.now().timestamp() + 20, + start_time: float | None = None, + end_time: float | None = None, severity: SeverityEnum = SeverityEnum.alert, - data: Json = {}, + 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="front_door", + camera=camera, start_time=start_time, end_time=end_time, severity=severity, @@ -173,11 +234,16 @@ class BaseTestHttp(unittest.TestCase): def insert_mock_recording( self, id: str, - start_time: float = datetime.datetime.now().timestamp(), - end_time: float = datetime.datetime.now().timestamp() + 20, + 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, diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index e7785a9d7..b04b1cf55 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -1,10 +1,8 @@ from unittest.mock import Mock -from fastapi.testclient import TestClient - from frigate.models import Event, Recordings, ReviewSegment from frigate.stats.emitter import StatsEmitter -from frigate.test.http_api.base_http_test import BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestHttpApp(BaseTestHttp): @@ -20,7 +18,7 @@ class TestHttpApp(BaseTestHttp): stats.get_latest_stats.return_value = self.test_stats app = super().create_app(stats) - with TestClient(app) as client: + with AuthTestClient(app) as client: response = client.get("/stats") response_json = response.json() assert response_json == self.test_stats 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 000000000..5cd115417 --- /dev/null +++ b/frigate/test/http_api/test_http_camera_access.py @@ -0,0 +1,192 @@ +from unittest.mock import patch + +from fastapi import HTTPException, Request + +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 + + +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 diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index e3f41fdc3..fc895fabf 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -1,34 +1,65 @@ from datetime import datetime +from typing import Any +from unittest.mock import Mock -from fastapi.testclient import TestClient +from playhouse.shortcuts import model_to_dict -from frigate.models import Event, Recordings, ReviewSegment -from frigate.test.http_api.base_http_test import BaseTestHttp +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]) + 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 TestClient(self.app) as client: + 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 TestClient(self.app) as client: + 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 TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_event(id) events = client.get("/events", params={"event_id": id}).json() assert len(events) == 1 @@ -38,7 +69,7 @@ class TestHttpApp(BaseTestHttp): now = int(datetime.now().timestamp()) id = "123456.random" - with TestClient(self.app) as client: + 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} @@ -49,7 +80,7 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_max_length(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + 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() @@ -58,23 +89,24 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_min_length(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + 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 TestClient(self.app) as client: - super().insert_mock_event(id) + 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) + super().insert_mock_event(id2, start_time=now) events = client.get("/events").json() assert len(events) == 2 @@ -88,14 +120,14 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_no_match_has_clip(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + 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 TestClient(self.app) as client: + 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() @@ -103,7 +135,7 @@ class TestHttpApp(BaseTestHttp): assert events[0]["id"] == id def test_get_event_list_sort_score(self): - with TestClient(self.app) as client: + 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}) @@ -113,7 +145,7 @@ class TestHttpApp(BaseTestHttp): assert events[0]["id"] == id2 assert events[1]["id"] == id - events = client.get("/events", params={"sort": "score_des"}).json() + events = client.get("/events", params={"sort": "score_desc"}).json() assert len(events) == 2 assert events[0]["id"] == id assert events[1]["id"] == id2 @@ -121,7 +153,7 @@ class TestHttpApp(BaseTestHttp): def test_get_event_list_sort_start_time(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" id2 = "54321.random" super().insert_mock_event(id, start_time=now + 3) @@ -135,3 +167,247 @@ class TestHttpApp(BaseTestHttp): assert len(events) == 2 assert events[0]["id"] == id assert events[1]["id"] == id2 + + 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_media.py b/frigate/test/http_api/test_http_media.py new file mode 100644 index 000000000..6af3dd972 --- /dev/null +++ b/frigate/test/http_api/test_http_media.py @@ -0,0 +1,405 @@ +"""Unit tests for recordings/media API endpoints.""" + +from datetime import datetime, timezone + +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=timezone.utc).timestamp() + march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.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 diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py index 469e012b2..ca73c8706 100644 --- a/frigate/test/http_api/test_http_review.py +++ b/frigate/test/http_api/test_http_review.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta -from fastapi.testclient import TestClient +from fastapi import Request from peewee import DoesNotExist -from frigate.api.auth import get_current_user +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 BaseTestHttp +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp class TestHttpReview(BaseTestHttp): @@ -16,11 +16,27 @@ class TestHttpReview(BaseTestHttp): self.user_id = "admin" # Mock get_current_user for all tests - async def mock_get_current_user(): - return {"username": self.user_id, "role": "admin"} + # 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() @@ -53,7 +69,7 @@ class TestHttpReview(BaseTestHttp): but ends after is included in the results.""" now = datetime.now().timestamp() - with TestClient(self.app) as client: + 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 @@ -63,7 +79,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_no_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now - 2, now - 1) response = client.get("/review") @@ -77,7 +93,7 @@ class TestHttpReview(BaseTestHttp): """Test that review items outside the range are not returned.""" now = datetime.now().timestamp() - with TestClient(self.app) as client: + 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) @@ -93,7 +109,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_time_filter(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2) params = { @@ -109,7 +125,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_limit_filter(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" id2 = "654321.random" super().insert_mock_review_segment(id, now, now + 2) @@ -128,7 +144,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_severity_filters_no_matches(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) params = { @@ -145,7 +161,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_severity_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) params = { @@ -161,7 +177,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_with_all_filters(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id, now, now + 2) params = { @@ -180,11 +196,55 @@ class TestHttpReview(BaseTestHttp): 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 TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") params = { "cameras": "front_door", @@ -215,7 +275,7 @@ class TestHttpReview(BaseTestHttp): self.assertEqual(response_json, expected_response) def test_get_review_summary_no_filters(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") response = client.get("/review/summary") assert response.status_code == 200 @@ -243,7 +303,7 @@ class TestHttpReview(BaseTestHttp): now = datetime.now() five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment( "123456.random", now.timestamp() - 2, now.timestamp() - 1 ) @@ -287,7 +347,7 @@ class TestHttpReview(BaseTestHttp): now = datetime.now() five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + 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): @@ -338,7 +398,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review_summary_multiple_in_same_day_with_reviewed(self): five_days_ago = datetime.today() - timedelta(days=5) - with TestClient(self.app) as client: + 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" @@ -389,14 +449,14 @@ class TestHttpReview(BaseTestHttp): #################################################################################################################### def test_post_reviews_viewed_no_body(self): - with TestClient(self.app) as client: + 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 TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") body = {"ids": [""]} response = client.post("/reviews/viewed", json=body) @@ -404,7 +464,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 422 def test_post_reviews_viewed_non_existent_id(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": ["1"]} @@ -412,7 +472,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 200 response = response.json() assert response["success"] == True - assert response["message"] == "Reviewed multiple items" + assert response["message"] == "Marked multiple items as reviewed" # Verify that in DB the review segment was not changed with self.assertRaises(DoesNotExist): UserReviewStatus.get( @@ -421,7 +481,7 @@ class TestHttpReview(BaseTestHttp): ) def test_post_reviews_viewed(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": [id]} @@ -429,7 +489,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 200 response_json = response.json() assert response_json["success"] == True - assert response_json["message"] == "Reviewed multiple items" + assert response_json["message"] == "Marked multiple items as reviewed" # Verify UserReviewStatus was created user_review = UserReviewStatus.get( UserReviewStatus.user_id == self.user_id, @@ -441,14 +501,14 @@ class TestHttpReview(BaseTestHttp): ################################### POST reviews/delete Endpoint ################################################ #################################################################################################################### def test_post_reviews_delete_no_body(self): - with TestClient(self.app) as client: + 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 TestClient(self.app) as client: + with AuthTestClient(self.app) as client: super().insert_mock_review_segment("123456.random") body = {"ids": [""]} response = client.post( @@ -458,7 +518,7 @@ class TestHttpReview(BaseTestHttp): assert response.status_code == 422 def test_post_reviews_delete_non_existent_id(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": ["1"]} @@ -475,7 +535,7 @@ class TestHttpReview(BaseTestHttp): assert review_ids_in_db_after[0].id == id def test_post_reviews_delete(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: id = "123456.random" super().insert_mock_review_segment(id) body = {"ids": [id]} @@ -491,7 +551,7 @@ class TestHttpReview(BaseTestHttp): assert len(review_ids_in_db_after) == 0 def test_post_reviews_delete_many(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: ids = ["123456.random", "654321.random"] for id in ids: super().insert_mock_review_segment(id) @@ -523,7 +583,7 @@ class TestHttpReview(BaseTestHttp): def test_review_activity_motion_no_data_for_time_range(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: params = { "after": now, "before": now + 3, @@ -536,7 +596,7 @@ class TestHttpReview(BaseTestHttp): def test_review_activity_motion(self): now = int(datetime.now().timestamp()) - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: one_m = int((datetime.now() + timedelta(minutes=1)).timestamp()) id = "123456.random" id2 = "123451.random" @@ -569,7 +629,7 @@ class TestHttpReview(BaseTestHttp): ################################### GET /review/event/{event_id} Endpoint ####################################### #################################################################################################################### def test_review_event_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: response = client.get("/review/event/123456.random") assert response.status_code == 404 response_json = response.json() @@ -581,7 +641,7 @@ class TestHttpReview(BaseTestHttp): def test_review_event_not_found_in_data(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + 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}") @@ -595,7 +655,7 @@ class TestHttpReview(BaseTestHttp): def test_review_get_specific_event(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: event_id = "123456.event.random" super().insert_mock_event(event_id) review_id = "123456.review.random" @@ -622,7 +682,7 @@ class TestHttpReview(BaseTestHttp): ################################### GET /review/{review_id} Endpoint ####################################### #################################################################################################################### def test_review_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: response = client.get("/review/123456.random") assert response.status_code == 404 response_json = response.json() @@ -634,7 +694,7 @@ class TestHttpReview(BaseTestHttp): def test_get_review(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + 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}") @@ -658,7 +718,7 @@ class TestHttpReview(BaseTestHttp): #################################################################################################################### def test_delete_review_viewed_review_not_found(self): - with TestClient(self.app) as client: + with AuthTestClient(self.app) as client: review_id = "123456.random" response = client.delete(f"/review/{review_id}/viewed") assert response.status_code == 404 @@ -671,7 +731,7 @@ class TestHttpReview(BaseTestHttp): def test_delete_review_viewed(self): now = datetime.now().timestamp() - with TestClient(self.app) as client: + 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) diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 4bafe7369..afe577f2f 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -632,6 +632,49 @@ class TestConfig(unittest.TestCase): ) 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"}, diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py deleted file mode 100644 index 4d949c543..000000000 --- a/frigate/test/test_http.py +++ /dev/null @@ -1,393 +0,0 @@ -import datetime -import logging -import os -import unittest -from unittest.mock import Mock - -from fastapi.testclient import TestClient -from peewee_migrate import Router -from playhouse.shortcuts import model_to_dict -from playhouse.sqlite_ext import SqliteExtDatabase -from playhouse.sqliteq import SqliteQueueDatabase - -from frigate.api.fastapi_app import create_fastapi_app -from frigate.comms.event_metadata_updater import EventMetadataPublisher -from frigate.config import FrigateConfig -from frigate.const import BASE_DIR, CACHE_DIR -from frigate.models import Event, Recordings, Timeline -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, Timeline] - self.db.bind(models) - - 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 = { - "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 test_get_good_event(self): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - id = "123456.random" - - with TestClient(app) as client: - _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): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - id = "123456.random" - bad_id = "654321.other" - - with TestClient(app) as client: - _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): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - id = "123456.random" - - with TestClient(app) as client: - _insert_mock_event(id) - event = client.get(f"/events/{id}").json() - assert event - assert event["id"] == id - client.delete(f"/events/{id}", headers={"remote-role": "admin"}) - event = client.get(f"/events/{id}").json() - assert event == "Event not found" - - def test_event_retention(self): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - id = "123456.random" - - with TestClient(app) as client: - _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): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - 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 TestClient(app) as client: - _insert_mock_event(morning_id, morning) - _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 = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - mock_event_updater, - ) - id = "123456.random" - sub_label = "sub" - - def update_event(topic, payload): - event = Event.get(id=id) - event.sub_label = payload[1] - event.save() - - mock_event_updater.publish.side_effect = update_event - - with TestClient(app) as client: - _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 = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - mock_event_updater, - ) - id = "123456.random" - sub_label = "sub" - - def update_event(topic, payload): - event = Event.get(id=id) - event.sub_label = payload[1] - event.save() - - mock_event_updater.publish.side_effect = update_event - - with TestClient(app) as client: - _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] - - def test_config(self): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - - with TestClient(app) as client: - config = client.get("/config").json() - assert config - assert config["cameras"]["front_door"] - - def test_recordings(self): - app = create_fastapi_app( - FrigateConfig(**self.minimal_config), - self.db, - None, - None, - None, - None, - None, - None, - ) - id = "123456.random" - - with TestClient(app) as client: - _insert_mock_recording(id) - response = client.get("/front_door/recordings") - assert response.status_code == 200 - recording = response.json() - assert recording - assert recording[0]["id"] == id - - -def _insert_mock_event( - id: str, - start_time: datetime.datetime = datetime.datetime.now().timestamp(), -) -> Event: - """Inserts a basic event model with a given id.""" - return Event.insert( - id=id, - label="Mock", - camera="front_door", - start_time=start_time, - end_time=start_time + 20, - top_score=100, - false_positive=False, - zones=list(), - thumbnail="", - region=[], - box=[], - area=0, - has_clip=True, - has_snapshot=True, - ).execute() - - -def _insert_mock_recording(id: str) -> Event: - """Inserts a basic recording model with a given id.""" - return Recordings.insert( - id=id, - camera="front_door", - path=f"/recordings/{id}", - start_time=datetime.datetime.now().timestamp() - 60, - end_time=datetime.datetime.now().timestamp() - 50, - duration=10, - motion=True, - objects=True, - ).execute() diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py new file mode 100644 index 000000000..d978cfd9f --- /dev/null +++ b/frigate/test/test_maintainer.py @@ -0,0 +1,66 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock complex imports before importing maintainer +sys.modules["frigate.comms.inter_process"] = MagicMock() +sys.modules["frigate.comms.detections_updater"] = MagicMock() +sys.modules["frigate.comms.recordings_updater"] = MagicMock() +sys.modules["frigate.config.camera.updater"] = MagicMock() + +# Now import the class under test +from frigate.config import FrigateConfig # noqa: E402 +from frigate.record.maintainer import RecordingMaintainer # noqa: E402 + + +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)}", + ) + + +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 000000000..61955486a --- /dev/null +++ b/frigate/test/test_proxy_auth.py @@ -0,0 +1,78 @@ +import unittest + +from frigate.api.auth import resolve_role +from frigate.config import HeaderMappingConfig, ProxyConfig + + +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_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) diff --git a/frigate/test/test_record_retention.py b/frigate/test/test_record_retention.py index b9aead9da..b826c3afb 100644 --- a/frigate/test/test_record_retention.py +++ b/frigate/test/test_record_retention.py @@ -12,11 +12,11 @@ class TestRecordRetention(unittest.TestCase): assert not segment_info.should_discard_segment(RetainModeEnum.motion) assert segment_info.should_discard_segment(RetainModeEnum.active_objects) - def test_object_should_keep_object_not_motion(self): + 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 segment_info.should_discard_segment(RetainModeEnum.motion) + 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): diff --git a/frigate/test/test_storage.py b/frigate/test/test_storage.py index d36960f47..4ae5715ca 100644 --- a/frigate/test/test_storage.py +++ b/frigate/test/test_storage.py @@ -261,12 +261,19 @@ class TestHttp(unittest.TestCase): assert Recordings.get(Recordings.id == rec_k3_id) -def _insert_mock_event(id: str, start: int, end: int, retain: bool) -> Event: +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="Mock", - camera="front_door", + label=label, + camera=camera, start_time=start, end_time=end, top_score=100, diff --git a/frigate/timeline.py b/frigate/timeline.py index 4e3c8e293..cf2f5e8c7 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -86,11 +86,11 @@ class TimelineProcessor(threading.Thread): event_data: dict[Any, Any], ) -> bool: """Handle object detection.""" - save = False camera_config = self.config.cameras[camera] event_id = event_data["id"] - timeline_entry = { + # Base timeline entry data that all entries will share + base_entry = { Timeline.timestamp: event_data["frame_time"], Timeline.camera: camera, Timeline.source: "tracked_object", @@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread): event_data["region"], ), "attribute": "", + "score": event_data["score"], }, } @@ -122,32 +123,64 @@ class TimelineProcessor(threading.Thread): 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" - save = True + 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 ( - len(prev_event_data["current_zones"]) < len(event_data["current_zones"]) + zones_changed and not event_data["stationary"] + and len(current_zones) > 0 ): - timeline_entry[Timeline.class_type] = "entered_zone" - timeline_entry[Timeline.data]["zones"] = event_data["current_zones"] - save = True - elif prev_event_data["stationary"] != event_data["stationary"]: - timeline_entry[Timeline.class_type] = ( + 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" ) - save = True - elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}: - timeline_entry[Timeline.class_type] = "attribute" - timeline_entry[Timeline.data]["attribute"] = list( + 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] - save = True - elif event_type == EventStateEnum.end: - timeline_entry[Timeline.class_type] = "gone" - save = True - if save: + 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( @@ -156,7 +189,7 @@ class TimelineProcessor(threading.Thread): event_type: str, event_data: dict[Any, Any], ) -> bool: - if event_type != "new": + if event_type != "start": return False if event_data.get("type", "api") == "audio": diff --git a/frigate/track/__init__.py b/frigate/track/__init__.py index dc72be4f0..b5453aaeb 100644 --- a/frigate/track/__init__.py +++ b/frigate/track/__init__.py @@ -11,6 +11,9 @@ class ObjectTracker(ABC): @abstractmethod def match_and_update( - self, frame_name: str, frame_time: float, detections: list[dict[str, Any]] + 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 index 25d4cb860..56f20629c 100644 --- a/frigate/track/centroid_tracker.py +++ b/frigate/track/centroid_tracker.py @@ -1,25 +1,26 @@ 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 import intersection_over_union +from frigate.util.image import intersection_over_union class CentroidTracker(ObjectTracker): def __init__(self, config: DetectConfig): - self.tracked_objects = {} - self.untracked_object_boxes = [] - self.disappeared = {} - self.positions = {} + 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, index, obj): + 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 @@ -39,13 +40,13 @@ class CentroidTracker(ObjectTracker): "ymax": self.detect_config.height, } - def deregister(self, id): + 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, box): + def update_position(self, id: str, box: tuple[int, int, int, int]) -> bool: position = self.positions[id] position_box = ( position["xmin"], @@ -88,7 +89,7 @@ class CentroidTracker(ObjectTracker): return True - def is_expired(self, id): + 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( @@ -108,7 +109,7 @@ class CentroidTracker(ObjectTracker): return False - def update(self, id, new_obj): + 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"]): @@ -129,25 +130,30 @@ class CentroidTracker(ObjectTracker): self.tracked_objects[id].update(new_obj) - def update_frame_times(self, frame_name, frame_time): + 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_time, detections): + 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 obj in detections: - detection_groups[obj[0]].append( + for det in detections: + detection_groups[det[0]].append( { - "label": obj[0], - "score": obj[1], - "box": obj[2], - "area": obj[3], - "ratio": obj[4], - "region": obj[5], + "label": det[0], + "score": det[1], + "box": det[2], + "area": det[3], + "ratio": det[4], + "region": det[5], "frame_time": frame_time, } ) @@ -180,7 +186,7 @@ class CentroidTracker(ObjectTracker): if len(current_objects) == 0: for index, obj in enumerate(group): - self.register(index, obj) + self.register(obj) continue new_centroids = np.array([o["centroid"] for o in group]) @@ -238,4 +244,4 @@ class CentroidTracker(ObjectTracker): # register each new input centroid as a trackable object else: for col in unusedCols: - self.register(col, group[col]) + self.register(group[col]) diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 900971e0d..84a0f390a 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -1,18 +1,14 @@ import logging import random import string -from typing import Any, Sequence +from typing import Any, Sequence, cast import cv2 import numpy as np -from norfair import ( - Detection, - Drawable, - OptimizedKalmanFilterFactory, - Tracker, - draw_boxes, -) -from norfair.drawing.drawer import Drawer +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 @@ -21,6 +17,11 @@ 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, @@ -31,19 +32,13 @@ from frigate.util.object import average_boxes, median_of_boxes logger = logging.getLogger(__name__) -THRESHOLD_KNOWN_ACTIVE_IOU = 0.2 -THRESHOLD_STATIONARY_CHECK_IOU = 0.6 -THRESHOLD_ACTIVE_CHECK_IOU = 0.9 -MAX_STATIONARY_HISTORY = 10 - - # 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.array, estimate: np.array) -> float: +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 @@ -73,14 +68,16 @@ def distance(detection: np.array, estimate: np.array) -> float: change = np.append(distance, np.array([width_ratio, height_ratio])) # calculate euclidean distance of the change vector - return np.linalg.norm(change) + return float(np.linalg.norm(change)) -def frigate_distance(detection: Detection, tracked_object) -> float: +def frigate_distance(detection: Detection, tracked_object: TrackedObject) -> float: return distance(detection.points, tracked_object.estimate) -def histogram_distance(matched_not_init_trackers, unmatched_trackers): +def histogram_distance( + matched_not_init_trackers: TrackedObject, unmatched_trackers: TrackedObject +) -> float: snd_embedding = unmatched_trackers.last_detection.embedding if snd_embedding is None: @@ -110,17 +107,18 @@ class NorfairTracker(ObjectTracker): ptz_metrics: PTZMetrics, ): self.frame_manager = SharedMemoryFrameManager() - self.tracked_objects = {} + self.tracked_objects: dict[str, dict[str, Any]] = {} self.untracked_object_boxes: list[list[int]] = [] - self.disappeared = {} - self.positions = {} - self.stationary_box_history: dict[str, list[list[int, int, int, 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 = {} + self.ptz_motion_estimator: PtzMotionEstimator | None = None self.camera_name = config.name - self.track_id_map = {} + self.track_id_map: dict[str, str] = {} + self.stationary_classifier = StationaryMotionClassifier() # Define tracker configurations for static camera self.object_type_configs = { @@ -169,7 +167,7 @@ class NorfairTracker(ObjectTracker): "distance_threshold": 3, } - self.trackers = {} + 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: @@ -195,19 +193,21 @@ class NorfairTracker(ObjectTracker): self.default_tracker = { "static": Tracker( distance_function=frigate_distance, - distance_threshold=self.default_tracker_config["distance_threshold"], + 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, - filter_factory=self.default_tracker_config["filter_factory"], + 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, - filter_factory=self.default_ptz_tracker_config["filter_factory"], + 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] ), } @@ -216,7 +216,7 @@ class NorfairTracker(ObjectTracker): self.camera_config, self.ptz_metrics ) - def _create_tracker(self, obj_type, tracker_config): + 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"], @@ -258,7 +258,7 @@ class NorfairTracker(ObjectTracker): return self.trackers[object_type][mode] return self.default_tracker[mode] - def register(self, track_id, obj): + 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 @@ -270,7 +270,7 @@ class NorfairTracker(ObjectTracker): # 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 o.global_id == track_id), None + (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"] = ( @@ -297,7 +297,7 @@ class NorfairTracker(ObjectTracker): } self.stationary_box_history[id] = boxes - def deregister(self, id, track_id): + def deregister(self, id: str, track_id: str) -> None: obj = self.tracked_objects[id] del self.tracked_objects[id] @@ -314,30 +314,22 @@ class NorfairTracker(ObjectTracker): tracker.tracked_objects = [ o for o in tracker.tracked_objects - if o.global_id != track_id and o.hit_counter < 0 + 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, int, int, int], stationary: bool): - xmin, ymin, xmax, ymax = box - position = self.positions[id] - self.stationary_box_history[id].append(box) - - if len(self.stationary_box_history[id]) > MAX_STATIONARY_HISTORY: - self.stationary_box_history[id] = self.stationary_box_history[id][ - -MAX_STATIONARY_HISTORY: - ] - - avg_iou = intersection_over_union( - box, average_boxes(self.stationary_box_history[id]) - ) - - # object has minimal or zero iou - # assume object is active - if avg_iou < THRESHOLD_KNOWN_ACTIVE_IOU: + 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], @@ -348,13 +340,50 @@ class NorfairTracker(ObjectTracker): "xmax": xmax, "ymax": ymax, } - return False + + 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 = ( - THRESHOLD_STATIONARY_CHECK_IOU if stationary else THRESHOLD_ACTIVE_CHECK_IOU + thresholds.stationary_check_iou + if stationary + else thresholds.active_check_iou ) - # object has iou below threshold, check median to reduce outliers + # object has iou below threshold, check median and optionally crop similarity if avg_iou < threshold: median_iou = intersection_over_union( ( @@ -363,27 +392,26 @@ class NorfairTracker(ObjectTracker): position["xmax"], position["ymax"], ), - median_of_boxes(self.stationary_box_history[id]), + median_box, ) # if the median iou drops below the threshold # assume object is no longer stationary if median_iou < threshold: - self.positions[id] = { - "xmins": [xmin], - "ymins": [ymin], - "xmaxs": [xmax], - "ymaxs": [ymax], - "xmin": xmin, - "ymin": ymin, - "xmax": xmax, - "ymax": ymax, - } - return False + # 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 5 <= len(position["xmins"]) < 10: + if len(position["xmins"]) < 10: position["xmins"].append(xmin) position["ymins"].append(ymin) position["xmaxs"].append(xmax) @@ -396,7 +424,7 @@ class NorfairTracker(ObjectTracker): return True - def is_expired(self, id): + 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( @@ -416,7 +444,13 @@ class NorfairTracker(ObjectTracker): return False - def update(self, track_id, obj): + 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 = ( @@ -424,7 +458,7 @@ class NorfairTracker(ObjectTracker): >= 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): + 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) @@ -440,10 +474,11 @@ class NorfairTracker(ObjectTracker): 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): + 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 = [ ( @@ -460,10 +495,22 @@ class NorfairTracker(ObjectTracker): self.match_and_update(frame_name, frame_time, detections=detections) def match_and_update( - self, frame_name: str, frame_time: float, detections: list[dict[str, Any]] - ): + 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 = {} + 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: @@ -478,9 +525,6 @@ class NorfairTracker(ObjectTracker): embedding = None if self.ptz_metrics.autotracker_enabled.value: - yuv_frame = self.frame_manager.get( - frame_name, self.camera_config.frame_shape_yuv - ) embedding = get_histogram( yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3] ) @@ -551,28 +595,34 @@ class NorfairTracker(ObjectTracker): estimate = ( max(0, estimate[0]), max(0, estimate[1]), - min(self.detect_config.width - 1, estimate[2]), - min(self.detect_config.height - 1, estimate[3]), + min(self.detect_config.width - 1, estimate[2]), # type: ignore[operator] + min(self.detect_config.height - 1, estimate[3]), # type: ignore[operator] ) - obj = { + new_obj = { **t.last_detection.data, "estimate": estimate, "estimate_velocity": t.estimate_velocity, } - active_ids.append(t.global_id) - if t.global_id not in self.track_id_map: - self.register(t.global_id, obj) + 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[t.global_id] + 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"] = obj["estimate"] + self.tracked_objects[id]["estimate"] = new_obj["estimate"] # else update it else: - self.update(t.global_id, obj) + 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] @@ -585,7 +635,7 @@ class NorfairTracker(ObjectTracker): o[2] for o in detections if o[2] not in tracked_object_boxes ] - def print_objects_as_table(self, tracked_objects: Sequence): + def print_objects_as_table(self, tracked_objects: Sequence) -> None: """Used for helping in debugging""" print() console = Console() @@ -605,13 +655,13 @@ class NorfairTracker(ObjectTracker): ) console.print(table) - def debug_draw(self, frame, frame_time): + def debug_draw(self, frame: np.ndarray, frame_time: float) -> None: # Collect all tracked objects from each tracker - all_tracked_objects = [] + 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: + 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 ) @@ -638,9 +688,9 @@ class NorfairTracker(ObjectTracker): # 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) + 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) + 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 @@ -648,8 +698,8 @@ class NorfairTracker(ObjectTracker): ld = obj.last_detection # bottom right text_anchor = ( - ld.points[1, 0], - ld.points[1, 1], + ld.points[1, 0], # type: ignore[index] + ld.points[1, 1], # type: ignore[index] ) frame = Drawer.text( frame, @@ -662,7 +712,7 @@ class NorfairTracker(ObjectTracker): if False: # draw the current formatted time on the frame - from datetime import datetime + from datetime import datetime # type: ignore[unreachable] formatted_time = datetime.fromtimestamp(frame_time).strftime( "%m/%d/%Y %I:%M:%S %p" diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 773c6da30..e0ee74228 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -6,6 +6,7 @@ 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 @@ -14,7 +15,6 @@ import numpy as np from peewee import SQL, DoesNotExist from frigate.camera.state import CameraState -from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum from frigate.comms.dispatcher import Dispatcher from frigate.comms.event_metadata_updater import ( @@ -29,6 +29,10 @@ from frigate.config import ( RecordConfig, SnapshotsConfig, ) +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) from frigate.const import ( FAST_QUEUE_TIMEOUT, UPDATE_CAMERA_ACTIVITY, @@ -36,6 +40,7 @@ from frigate.const import ( ) 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 @@ -53,10 +58,10 @@ class TrackedObjectProcessor(threading.Thread): self, config: FrigateConfig, dispatcher: Dispatcher, - tracked_objects_queue, - ptz_autotracker_thread, - stop_event, - ): + tracked_objects_queue: MpQueue, + ptz_autotracker_thread: PtzAutoTrackerThread, + stop_event: MpEvent, + ) -> None: super().__init__(name="detected_frames_processor") self.config = config self.dispatcher = dispatcher @@ -67,10 +72,21 @@ class TrackedObjectProcessor(threading.Thread): self.last_motion_detected: dict[str, float] = {} self.ptz_autotracker_thread = ptz_autotracker_thread - self.config_enabled_subscriber = ConfigSubscriber("config/enabled/") + self.camera_config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.zones, + ], + ) self.requestor = InterProcessRequestor() - self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all) + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value) self.event_sender = EventUpdatePublisher() self.event_end_subscriber = EventEndSubscriber() self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all) @@ -86,10 +102,20 @@ class TrackedObjectProcessor(threading.Thread): # } # } # } - self.zone_data = defaultdict(lambda: defaultdict(dict)) - self.active_zone_data = defaultdict(lambda: defaultdict(dict)) + 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) + ) - def start(camera: str, obj: TrackedObject, frame_name: str): + 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, @@ -100,7 +126,7 @@ class TrackedObjectProcessor(threading.Thread): ) ) - def update(camera: str, obj: TrackedObject, frame_name: str): + 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() @@ -121,10 +147,10 @@ class TrackedObjectProcessor(threading.Thread): ) ) - def autotrack(camera: str, obj: TrackedObject, frame_name: str): + 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): + 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) @@ -193,26 +219,25 @@ class TrackedObjectProcessor(threading.Thread): return False - def camera_activity(camera, activity): + 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) - for camera in self.config.cameras.keys(): - 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 + 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, obj: TrackedObject): + def should_save_snapshot(self, camera: str, obj: TrackedObject) -> bool: if obj.false_positive: return False @@ -235,7 +260,7 @@ class TrackedObjectProcessor(threading.Thread): return True - def should_retain_recording(self, camera: str, obj: TrackedObject): + def should_retain_recording(self, camera: str, obj: TrackedObject) -> bool: if obj.false_positive: return False @@ -255,7 +280,7 @@ class TrackedObjectProcessor(threading.Thread): return True - def should_mqtt_snapshot(self, camera, obj: TrackedObject): + def should_mqtt_snapshot(self, camera: str, obj: TrackedObject) -> bool: # object never changed position if obj.is_stationary(): return False @@ -270,7 +295,9 @@ class TrackedObjectProcessor(threading.Thread): return True - def update_mqtt_motion(self, camera, frame_time, motion_boxes): + 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 @@ -296,11 +323,15 @@ class TrackedObjectProcessor(threading.Thread): # reset the last_motion so redundant `off` commands aren't sent self.last_motion_detected[camera] = 0 - def get_best(self, camera, label): + 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"] @@ -323,7 +354,7 @@ class TrackedObjectProcessor(threading.Thread): return self.camera_states[camera].get_current_frame(draw_options) - def get_current_frame_time(self, camera) -> int: + def get_current_frame_time(self, camera: str) -> float: """Returns the latest frame time for a given camera.""" return self.camera_states[camera].current_frame_time @@ -331,7 +362,7 @@ class TrackedObjectProcessor(threading.Thread): self, event_id: str, sub_label: str | None, score: float | None ) -> None: """Update sub label for given event id.""" - tracked_obj: TrackedObject = None + tracked_obj: TrackedObject | None = None for state in self.camera_states.values(): tracked_obj = state.tracked_objects.get(event_id) @@ -340,7 +371,7 @@ class TrackedObjectProcessor(threading.Thread): break try: - event: Event = Event.get(Event.id == event_id) + event: Event | None = Event.get(Event.id == event_id) except DoesNotExist: event = None @@ -351,12 +382,12 @@ class TrackedObjectProcessor(threading.Thread): tracked_obj.obj_data["sub_label"] = (sub_label, score) if event: - event.sub_label = sub_label + event.sub_label = sub_label # type: ignore[assignment] data = event.data if sub_label is None: - data["sub_label_score"] = None + data["sub_label_score"] = None # type: ignore[index] elif score is not None: - data["sub_label_score"] = score + data["sub_label_score"] = score # type: ignore[index] event.data = data event.save() @@ -385,7 +416,7 @@ class TrackedObjectProcessor(threading.Thread): objects_list = [] sub_labels = set() events = Event.select(Event.id, Event.label, Event.sub_label).where( - Event.id.in_(detection_ids) + Event.id.in_(detection_ids) # type: ignore[call-arg, misc] ) for det_event in events: if det_event.sub_label: @@ -414,18 +445,20 @@ class TrackedObjectProcessor(threading.Thread): f"Updated sub_label for event {event_id} in review segment {review_segment.id}" ) - except ReviewSegment.DoesNotExist: + except DoesNotExist: logger.debug( f"No review segment found with event ID {event_id} when updating sub_label" ) - return True - - def set_recognized_license_plate( - self, event_id: str, recognized_license_plate: str | None, score: float | None + def set_object_attribute( + self, + event_id: str, + field_name: str, + field_value: str | None, + score: float | None, ) -> None: - """Update recognized license plate for given event id.""" - tracked_obj: TrackedObject = 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) @@ -434,7 +467,7 @@ class TrackedObjectProcessor(threading.Thread): break try: - event: Event = Event.get(Event.id == event_id) + event: Event | None = Event.get(Event.id == event_id) except DoesNotExist: event = None @@ -442,23 +475,21 @@ class TrackedObjectProcessor(threading.Thread): return if tracked_obj: - tracked_obj.obj_data["recognized_license_plate"] = ( - recognized_license_plate, + tracked_obj.obj_data[field_name] = ( + field_value, score, ) if event: data = event.data - data["recognized_license_plate"] = recognized_license_plate - if recognized_license_plate is None: - data["recognized_license_plate_score"] = None + 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["recognized_license_plate_score"] = score + data[f"{field_name}_score"] = score # type: ignore[index] event.data = data event.save() - return True - def save_lpr_snapshot(self, payload: tuple) -> None: # save the snapshot image (frame, event_id, camera) = payload @@ -617,7 +648,7 @@ class TrackedObjectProcessor(threading.Thread): ) self.ongoing_manual_events.pop(event_id) - def force_end_all_events(self, camera: str, camera_state: CameraState): + 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()): @@ -635,27 +666,28 @@ class TrackedObjectProcessor(threading.Thread): {"enabled": False, "motion": 0, "objects": []}, ) - def run(self): + def run(self) -> None: while not self.stop_event.is_set(): # check for config updates - while True: - ( - updated_enabled_topic, - updated_enabled_config, - ) = self.config_enabled_subscriber.check_for_update() + updated_topics = self.camera_config_subscriber.check_for_updates() - if not updated_enabled_topic: - break - - camera_name = updated_enabled_topic.rpartition("/")[-1] - self.config.cameras[ - camera_name - ].enabled = updated_enabled_config.enabled - - if self.camera_states[camera_name].prev_enabled is None: - self.camera_states[ - camera_name - ].prev_enabled = updated_enabled_config.enabled + 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"]: + camera_state = self.camera_states[camera] + camera_state.shutdown() + self.camera_states.pop(camera) # manage camera disabled state for camera, config in self.config.cameras.items(): @@ -676,11 +708,14 @@ class TrackedObjectProcessor(threading.Thread): # check for sub label updates while True: - (raw_topic, payload) = self.sub_label_subscriber.check_for_update( - timeout=0 - ) + update = self.sub_label_subscriber.check_for_update(timeout=0) - if not raw_topic: + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: break topic = str(raw_topic) @@ -688,11 +723,9 @@ class TrackedObjectProcessor(threading.Thread): 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.recognized_license_plate.value): - (event_id, recognized_license_plate, score) = payload - self.set_recognized_license_plate( - event_id, recognized_license_plate, 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): @@ -764,6 +797,6 @@ class TrackedObjectProcessor(threading.Thread): self.event_sender.stop() self.event_end_subscriber.stop() self.sub_label_subscriber.stop() - self.config_enabled_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 000000000..832df5d31 --- /dev/null +++ b/frigate/track/stationary_classifier.py @@ -0,0 +1,254 @@ +"""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, +) + + +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 + + return StationaryThresholds() + + +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 index 2cb028a9a..453798651 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -5,18 +5,19 @@ import math import os from collections import defaultdict from statistics import median -from typing import Any, Optional +from typing import Any, Optional, cast import cv2 import numpy as np from frigate.config import ( CameraConfig, - ModelConfig, + FilterConfig, SnapshotsConfig, UIConfig, ) from frigate.const import CLIPS_DIR, 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 ( @@ -32,17 +33,25 @@ 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, + 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 = obj_data["score_history"] + self.score_history: list[float] = obj_data["score_history"] del obj_data["score_history"] self.obj_data = obj_data @@ -53,24 +62,24 @@ class TrackedObject: self.frame_cache = frame_cache self.zone_presence: dict[str, int] = {} self.zone_loitering: dict[str, int] = {} - self.current_zones = [] - self.entered_zones = [] - self.attributes = defaultdict(float) + self.current_zones: list[str] = [] + self.entered_zones: list[str] = [] + 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 = None + self.thumbnail_data: dict[str, Any] | None = None self.last_updated = 0 self.last_published = 0 self.frame = None self.active = True self.pending_loitering = False - self.speed_history = [] - self.current_estimated_speed = 0 - self.average_estimated_speed = 0 + self.speed_history: list[float] = [] + self.current_estimated_speed: float = 0 + self.average_estimated_speed: float = 0 self.velocity_angle = 0 - self.path_data = [] + self.path_data: list[tuple[Any, float]] = [] self.previous = self.to_dict() @property @@ -103,7 +112,7 @@ class TrackedObject: return None - def _is_false_positive(self): + def _is_false_positive(self) -> bool: # once a true positive, always a true positive if not self.false_positive: return False @@ -111,11 +120,13 @@ class TrackedObject: threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold return self.computed_score < threshold - def compute_score(self): + 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, has_valid_frame: bool): + 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 @@ -247,8 +258,12 @@ class TrackedObject: 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, update loitering status - if zone.loitering_time > 0: + # 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 @@ -264,6 +279,10 @@ class TrackedObject: self.entered_zones.append(name) 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: @@ -289,7 +308,7 @@ class TrackedObject: 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) + max_logo = max(recognized_logos, key=recognized_logos.get) # type: ignore[arg-type] # don't overwrite sub label if it is already set if ( @@ -326,28 +345,30 @@ class TrackedObject: # update path width = self.camera_config.detect.width height = self.camera_config.detect.height - 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']}" + 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( @@ -355,7 +376,7 @@ class TrackedObject: ) return (thumb_update, significant_change, path_update, autotracker_update) - def to_dict(self): + def to_dict(self) -> dict[str, Any]: event = { "id": self.obj_data["id"], "camera": self.camera_config.name, @@ -397,10 +418,8 @@ class TrackedObject: return not self.is_stationary() def is_stationary(self) -> bool: - return ( - self.obj_data["motionless_count"] - > self.camera_config.detect.stationary.threshold - ) + 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( @@ -413,7 +432,7 @@ class TrackedObject: _, img = cv2.imencode(f".{ext}", np.zeros((175, 175, 3), np.uint8)) return img.tobytes() - def get_clean_png(self) -> bytes | None: + def get_clean_webp(self) -> bytes | None: if self.thumbnail_data is None: return None @@ -424,22 +443,24 @@ class TrackedObject: ) except KeyError: logger.warning( - f"Unable to create clean png because frame {self.thumbnail_data['frame_time']} is not in the cache" + f"Unable to create clean webp because frame {self.thumbnail_data['frame_time']} is not in the cache" ) return None - ret, png = cv2.imencode(".png", best_frame) + ret, webp = cv2.imencode( + ".webp", best_frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) if ret: - return png.tobytes() + return webp.tobytes() else: return None def get_img_bytes( self, ext: str, - timestamp=False, - bounding_box=False, - crop=False, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, height: int | None = None, quality: int | None = None, ) -> bytes | None: @@ -516,18 +537,18 @@ class TrackedObject: best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA ) if timestamp: - color = self.camera_config.timestamp_style.color + colors = self.camera_config.timestamp_style.color draw_timestamp( best_frame, self.thumbnail_data["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=(color.blue, color.green, color.red), + font_color=(colors.blue, colors.green, colors.red), position=self.camera_config.timestamp_style.position, ) - quality_params = None + quality_params = [] if ext == "jpg": quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality or 70] @@ -564,8 +585,8 @@ class TrackedObject: # write clean snapshot if enabled if snapshot_config.clean_copy: - png_bytes = self.get_clean_png() - if png_bytes is None: + webp_bytes = self.get_clean_webp() + if webp_bytes is None: logger.warning( f"Unable to save clean snapshot for {self.obj_data['id']}." ) @@ -573,13 +594,16 @@ class TrackedObject: with open( os.path.join( CLIPS_DIR, - f"{self.camera_config.name}-{self.obj_data['id']}-clean.png", + f"{self.camera_config.name}-{self.obj_data['id']}-clean.webp", ), "wb", ) as p: - p.write(png_bytes) + p.write(webp_bytes) def write_thumbnail_to_disk(self) -> None: + if not self.camera_config.name: + return + directory = os.path.join(THUMB_DIR, self.camera_config.name) if not os.path.exists(directory): @@ -587,11 +611,14 @@ class TrackedObject: thumb_bytes = self.get_thumbnail("webp") - with open(os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb") as f: - f.write(thumb_bytes) + 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): +def zone_filtered(obj: TrackedObject, object_config: dict[str, FilterConfig]) -> bool: object_name = obj.obj_data["label"] if object_name in object_config: @@ -641,9 +668,9 @@ class TrackedObjectAttribute: def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]: """Find the best attribute for each object and return its ID.""" - best_object_area = None - best_object_id = None - best_object_label = None + 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): diff --git a/frigate/types.py b/frigate/types.py index ee48cc02b..6c5135616 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -21,9 +21,13 @@ class ModelStatusTypesEnum(str, Enum): downloading = "downloading" downloaded = "downloaded" error = "error" + training = "training" + complete = "complete" + failed = "failed" class TrackedObjectUpdateTypesEnum(str, Enum): description = "description" face = "face" lpr = "lpr" + classification = "classification" diff --git a/frigate/util/__init__.py b/frigate/util/__init__.py index 307bf4f8b..e69de29bb 100644 --- a/frigate/util/__init__.py +++ b/frigate/util/__init__.py @@ -1,3 +0,0 @@ -from .process import Process - -__all__ = ["Process"] diff --git a/frigate/util/audio.py b/frigate/util/audio.py new file mode 100644 index 000000000..eede9c0ea --- /dev/null +++ b/frigate/util/audio.py @@ -0,0 +1,116 @@ +"""Utilities for creating and manipulating audio.""" + +import logging +import os +import subprocess as sp +from typing import Optional + +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, +) -> Optional[bytes]: + """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 index 52280ecd8..b1a76214b 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -5,7 +5,7 @@ import copy import datetime import logging import math -import multiprocessing as mp +import multiprocessing.queues import queue import re import shlex @@ -14,13 +14,10 @@ import urllib.parse from collections.abc import Mapping from multiprocessing.sharedctypes import Synchronized from pathlib import Path -from typing import Any, Optional, Tuple, Union -from zoneinfo import ZoneInfoNotFoundError +from typing import Any, Dict, Optional, Tuple, Union import numpy as np -import pytz from ruamel.yaml import YAML -from tzlocal import get_localzone from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS @@ -157,17 +154,6 @@ def load_labels(path: Optional[str], encoding="utf-8", prefill=91): return labels -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 to_relative_box( width: int, height: int, box: Tuple[int, int, int, int] ) -> Tuple[int | float, int | float, int | float, int | float]: @@ -184,25 +170,12 @@ def create_mask(frame_shape, mask): mask_img[:] = 255 -def update_yaml_from_url(file_path, url): - parsed_url = urllib.parse.urlparse(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} - +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(): - key_path = key_path_str.split(".") - 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 - + # use the string key as-is for updates dictionary if len(new_value_list) > 1: - update_yaml_file(file_path, key_path, new_value_list) + updates[key_path_str] = new_value_list else: value = new_value_list[0] try: @@ -210,10 +183,24 @@ def update_yaml_from_url(file_path, url): value = ast.literal_eval(value) if "," not in value else value except (ValueError, SyntaxError): pass - update_yaml_file(file_path, key_path, value) + updates[key_path_str] = value + return updates -def update_yaml_file(file_path, key_path, new_value): +def flatten_config_data( + config_data: Dict[str, Any], parent_key: str = "" +) -> Dict[str, Any]: + items = [] + for key, value in config_data.items(): + new_key = f"{parent_key}.{key}" if parent_key else key + if isinstance(value, dict): + items.extend(flatten_config_data(value, new_key).items()) + else: + items.append((new_key, value)) + return dict(items) + + +def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) @@ -226,7 +213,17 @@ def update_yaml_file(file_path, key_path, new_value): ) return - data = update_yaml(data, key_path, new_value) + # Apply all updates + for key_path_str, new_value in updates.items(): + key_path = key_path_str.split(".") + 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: @@ -287,34 +284,6 @@ def find_by_key(dictionary, target_key): return None -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.timezone.utc) + datetime.timedelta( - days=1 - ) - logger.warning( - "Using utc for maintenance due to missing or incorrect timezone set" - ) - - return tomorrow.replace(hour=hour, minute=0, second=0).astimezone( - datetime.timezone.utc - ) - - -def is_current_hour(timestamp: int) -> bool: - """Returns if timestamp is in the current UTC hour.""" - start_of_next_hour = ( - datetime.datetime.now(datetime.timezone.utc).replace( - minute=0, second=0, microsecond=0 - ) - + datetime.timedelta(hours=1) - ).timestamp() - return timestamp < start_of_next_hour - - 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(): @@ -327,14 +296,24 @@ def clear_and_unlink(file: Path, missing_ok: bool = True) -> None: file.unlink(missing_ok=missing_ok) -def empty_and_close_queue(q: mp.Queue): +def empty_and_close_queue(q): while True: try: q.get(block=True, timeout=0.5) - except queue.Empty: + 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() - return + except Exception: + pass def generate_color_palette(n): @@ -407,3 +386,19 @@ def sanitize_float(value): 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/classification.py b/frigate/util/classification.py new file mode 100644 index 000000000..643f77d3b --- /dev/null +++ b/frigate/util/classification.py @@ -0,0 +1,895 @@ +"""Util for classification models.""" + +import datetime +import json +import logging +import os +import random +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 +from frigate.util.image import get_image_from_recording +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, "r") 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.error(f"Training failed for {self.model_name}: {e}", exc_info=True) + 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/7.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") + 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}") + + import shutil + + 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/7.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. Retrieves thumbnails for selected events (with 33% center crop applied) + 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") + 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}") + + import shutil + + 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(): + sorted_events = sorted( + group_events, + key=lambda e: e.data.get("score", 0) if e.data else 0, + reverse=True, + ) + + sample_size = min(samples_per_group, len(sorted_events)) + selected.extend(sorted_events[:sample_size]) + + if len(selected) < target_count: + remaining = [e for e in events if e not in selected] + remaining_sorted = sorted( + remaining, + key=lambda e: e.data.get("score", 0) if e.data else 0, + reverse=True, + ) + needed = target_count - len(selected) + selected.extend(remaining_sorted[:needed]) + + return selected[:target_count] + + +def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]: + """ + Extract thumbnails from events and save to disk. + + Args: + events: List of Event objects + output_dir: Directory to save thumbnails + + Returns: + List of paths to successfully extracted thumbnail images + """ + thumbnail_paths = [] + + for idx, event in enumerate(events): + try: + thumbnail_bytes = get_event_thumbnail_bytes(event) + + if thumbnail_bytes: + nparr = np.frombuffer(thumbnail_bytes, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is not 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 + x2 = x1 + crop_width + y2 = y1 + crop_height + + cropped = img[y1:y2, x1:x2] + resized = cv2.resize(cropped, (224, 224)) + output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg") + cv2.imwrite(output_path, resized) + thumbnail_paths.append(output_path) + + except Exception as e: + logger.debug(f"Failed to extract thumbnail for event {event.id}: {e}") + continue + + return thumbnail_paths diff --git a/frigate/util/config.py b/frigate/util/config.py index 70492adbc..c3d796397 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -13,7 +13,7 @@ from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) -CURRENT_CONFIG_VERSION = "0.16-0" +CURRENT_CONFIG_VERSION = "0.17-0" DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") @@ -91,6 +91,13 @@ def migrate_frigate_config(config_file: str): 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" + logger.info("Finished frigate config migration...") @@ -340,6 +347,86 @@ def migrate_016_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] 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 get_relative_coordinates( mask: Optional[Union[str, list]], frame_shape: tuple[int, int] ) -> Union[str, list]: diff --git a/frigate/util/downloader.py b/frigate/util/downloader.py index 49b05dd05..ee80b3816 100644 --- a/frigate/util/downloader.py +++ b/frigate/util/downloader.py @@ -1,7 +1,6 @@ import logging import os import threading -import time from pathlib import Path from typing import Callable, List @@ -10,40 +9,11 @@ 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 FileLock: - def __init__(self, path): - self.path = path - self.lock_file = f"{path}.lock" - - # we have not acquired the lock yet so it should not exist - if os.path.exists(self.lock_file): - try: - os.remove(self.lock_file) - except Exception: - pass - - def acquire(self): - parent_dir = os.path.dirname(self.lock_file) - os.makedirs(parent_dir, exist_ok=True) - - while True: - try: - with open(self.lock_file, "x"): - return - except FileExistsError: - time.sleep(0.1) - - def release(self): - try: - os.remove(self.lock_file) - except FileNotFoundError: - pass - - class ModelDownloader: def __init__( self, @@ -81,15 +51,13 @@ class ModelDownloader: def _download_models(self): for file_name in self.file_names: path = os.path.join(self.download_path, file_name) - lock = FileLock(path) + lock_path = f"{path}.lock" + lock = FileLock(lock_path, cleanup_stale_on_init=True) if not os.path.exists(path): - lock.acquire() - try: + with lock: if not os.path.exists(path): self.download_func(path) - finally: - lock.release() self.requestor.send_data( UPDATE_MODEL_STATE, diff --git a/frigate/util/file.py b/frigate/util/file.py new file mode 100644 index 000000000..22be3e511 --- /dev/null +++ b/frigate/util/file.py @@ -0,0 +1,276 @@ +"""Path and file utilities.""" + +import base64 +import fcntl +import logging +import os +import time +from pathlib import Path +from typing import Optional + +import cv2 +from numpy import ndarray + +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.models import Event + +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: + media_name = f"{event.camera}-{event.id}" + return cv2.imread(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") + + +### 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: Optional[int] = 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: Optional[int] = 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, IOError): + # 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 index 58afe8b36..ea9fb0a0a 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -66,7 +66,12 @@ def has_better_attr(current_thumb, new_obj, attr_label) -> bool: return max_new_attr > max_current_attr -def is_better_thumbnail(label, current_thumb, new_obj, frame_shape) -> bool: +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 @@ -938,6 +943,58 @@ def add_mask(mask: str, mask_img: np.ndarray): cv2.fillPoly(mask_img, pts=[contour], color=(0)) +def run_ffmpeg_snapshot( + ffmpeg, + input_path: str, + codec: str, + seek_time: Optional[float] = None, + height: Optional[int] = None, + timeout: Optional[int] = None, +) -> tuple[Optional[bytes], str]: + """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, @@ -947,37 +1004,11 @@ def get_image_from_recording( ) -> Optional[Any]: """retrieve a frame from given time in recording file.""" - ffmpeg_cmd = [ - ffmpeg.ffmpeg_path, - "-hide_banner", - "-loglevel", - "warning", - "-ss", - f"00:00:{relative_frame_time}", - "-i", - file_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}") - - process = sp.run( - ffmpeg_cmd, - capture_output=True, + image_data, _ = run_ffmpeg_snapshot( + ffmpeg, file_path, codec, seek_time=relative_frame_time, height=height ) - if process.returncode == 0: - return process.stdout - else: - return None + return image_data def get_histogram(image, x_min, y_min, x_max, y_max): @@ -990,7 +1021,26 @@ def get_histogram(image, x_min, y_min, x_max, y_max): return cv2.normalize(hist, hist).flatten() -def ensure_jpeg_bytes(image_data): +def create_thumbnail( + yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500 +) -> Optional[bytes]: + """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) diff --git a/frigate/util/model.py b/frigate/util/model.py index 65f9b6032..338303e2d 100644 --- a/frigate/util/model.py +++ b/frigate/util/model.py @@ -284,7 +284,9 @@ def post_process_yolox( def get_ort_providers( - force_cpu: bool = False, device: str = "AUTO", requires_fp16: bool = False + force_cpu: bool = False, + device: str | None = "AUTO", + requires_fp16: bool = False, ) -> tuple[list[str], list[dict[str, Any]]]: if force_cpu: return ( @@ -301,11 +303,12 @@ def get_ort_providers( for provider in ort.get_available_providers(): if provider == "CUDAExecutionProvider": - device_id = 0 if not device.isdigit() else int(device) + 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, } ) @@ -337,21 +340,28 @@ def get_ort_providers( else: continue elif provider == "OpenVINOExecutionProvider": - os.makedirs(os.path.join(MODEL_CACHE_DIR, "openvino/ort"), exist_ok=True) + # 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( { - "cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"), - "device_type": device, + "migraphx_model_cache_dir": migraphx_cache_dir, } ) - elif provider == "MIGraphXExecutionProvider": - # MIGraphX uses more CPU than ROCM, while also being the same speed - if device == "MIGraphX": - providers.append(provider) - options.append({}) - else: - continue elif provider == "CPUExecutionProvider": providers.append(provider) options.append( @@ -359,6 +369,10 @@ def get_ort_providers( "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({}) diff --git a/frigate/util/object.py b/frigate/util/object.py index d9a8c2f71..905745da6 100644 --- a/frigate/util/object.py +++ b/frigate/util/object.py @@ -269,7 +269,20 @@ def is_object_filtered(obj, objects_to_track, object_filters): def get_min_region_size(model_config: ModelConfig) -> int: """Get the min region size.""" - return max(model_config.height, model_config.width) + largest_dimension = max(model_config.height, model_config.width) + + if largest_dimension > 320: + # We originally tested allowing any model to have a region down to half of the model size + # but this led to many false positives. In this case we specifically target larger models + # which can benefit from a smaller region in some cases to detect smaller objects. + half = int(largest_dimension / 2) + + if half % 4 == 0: + return half + + return int((half + 3) / 4) * 4 + + return largest_dimension def create_tensor_input(frame, model_config: ModelConfig, region): diff --git a/frigate/util/path.py b/frigate/util/path.py deleted file mode 100644 index 565f5a357..000000000 --- a/frigate/util/path.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Path utilities.""" - -import base64 -import os -from pathlib import Path - -import cv2 -from numpy import ndarray - -from frigate.const import CLIPS_DIR, THUMB_DIR -from frigate.models import Event - - -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: - media_name = f"{event.camera}-{event.id}" - return cv2.imread(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") - - -### 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.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 diff --git a/frigate/util/process.py b/frigate/util/process.py index ac15539fe..1613c1e43 100644 --- a/frigate/util/process.py +++ b/frigate/util/process.py @@ -1,19 +1,27 @@ +import atexit import faulthandler import logging import multiprocessing as mp -import signal -import sys +import os +import pathlib +import subprocess import threading -from functools import wraps from logging.handlers import QueueHandler -from typing import Any, Callable, Optional +from multiprocessing.synchronize import Event as MpEvent +from typing import Callable, Optional + +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: Optional[str] = None, target: Optional[Callable] = None, @@ -21,6 +29,8 @@ class BaseProcess(mp.Process): kwargs: dict = {}, daemon: Optional[bool] = None, ): + self.priority = priority + self.stop_event = stop_event super().__init__( name=name, target=target, args=args, kwargs=kwargs, daemon=daemon ) @@ -30,66 +40,120 @@ class BaseProcess(mp.Process): super().start(*args, **kwargs) self.after_start() - def __getattribute__(self, name: str) -> Any: - if name == "run": - run = super().__getattribute__("run") - - @wraps(run) - def run_wrapper(*args, **kwargs): - try: - self.before_run() - return run(*args, **kwargs) - finally: - self.after_run() - - return run_wrapper - - return super().__getattribute__(name) - def before_start(self) -> None: pass def after_start(self) -> None: pass - def before_run(self) -> None: - pass - def after_run(self) -> None: - pass - - -class Process(BaseProcess): +class FrigateProcess(BaseProcess): 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__: - self.__dict__["stop_event"] = threading.Event() - return self.__dict__["stop_event"] - def before_start(self) -> None: self.__log_queue = frigate.log.log_listener.queue + self.__memray_tracker = None - def before_run(self) -> 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() - def receiveSignal(signalNumber, frame): - # 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) - + # 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.error(f"Failed to setup memray profiling: {e}", exc_info=True) + + 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.error(f"Failed to cleanup memray profiling: {e}", exc_info=True) diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py new file mode 100644 index 000000000..f7ebbf5e6 --- /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 typing import Optional + +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", + "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() -> Optional[str]: + """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: Optional[str] = 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 +) -> Optional[str]: + """ + 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/services.py b/frigate/util/services.py index b31a7eea3..64d83833d 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -6,8 +6,10 @@ 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, List, Optional, Tuple @@ -22,6 +24,7 @@ from frigate.const import ( DRIVER_ENV_VAR, FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_VAAPI, + SHM_FRAMES_VAR, ) from frigate.util.builtin import clean_camera_user_pass, escape_special_characters @@ -386,6 +389,39 @@ def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, s return results +def get_openvino_npu_stats() -> Optional[dict[str, str]]: + """Get NPU stats using openvino.""" + NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time" + + try: + with open(NPU_RUNTIME_PATH, "r") as f: + 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, "r") 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() -> Optional[dict[str, str]]: """Get GPU stats using rk.""" try: @@ -504,18 +540,36 @@ def get_jetson_stats() -> Optional[dict[int, dict]]: try: results["mem"] = "-" # no discrete gpu memory - with open("/sys/devices/gpu.0/load", "r") as f: - gpuload = float(f.readline()) / 10 - results["gpu"] = f"{gpuload}%" + if os.path.exists("/sys/devices/gpu.0/load"): + with open("/sys/devices/gpu.0/load", "r") as f: + gpuload = float(f.readline()) / 10 + results["gpu"] = f"{gpuload}%" + elif os.path.exists("/sys/devices/platform/gpu.0/load"): + with open("/sys/devices/platform/gpu.0/load", "r") as f: + gpuload = float(f.readline()) / 10 + results["gpu"] = f"{gpuload}%" + else: + results["gpu"] = "-" except Exception: return None return results -def ffprobe_stream(ffmpeg, path: str) -> sp.CompletedProcess: +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 + ffprobe_cmd = [ ffmpeg.ffprobe_path, "-timeout", @@ -523,11 +577,15 @@ def ffprobe_stream(ffmpeg, path: str) -> sp.CompletedProcess: "-print_format", "json", "-show_entries", - "stream=codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate", - "-loglevel", - "quiet", - clean_path, + f"stream={stream_entries}", ] + + # Add format entries for detailed mode + if detailed and format_entries: + ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"]) + + ffprobe_cmd.extend(["-loglevel", "error", clean_path]) + return sp.run(ffprobe_cmd, capture_output=True) @@ -601,87 +659,87 @@ def auto_detect_hwaccel() -> str: async def get_video_properties( ffmpeg, url: str, get_duration: bool = False ) -> dict[str, Any]: - async def calculate_duration(video: Optional[Any]) -> float: - duration = None - - if video is not None: - # Get the frames per second (fps) of the video stream - fps = video.get(cv2.CAP_PROP_FPS) - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - - if fps and total_frames: - duration = total_frames / fps - - # if cv2 failed need to use ffprobe - if duration is None: - p = await asyncio.create_subprocess_exec( - ffmpeg.ffprobe_path, - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - f"{url}", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + async def probe_with_ffprobe( + url: str, + ) -> tuple[bool, int, int, Optional[str], float]: + """Fallback using ffprobe: returns (valid, width, height, codec, duration).""" + cmd = [ + ffmpeg.ffprobe_path, + "-v", + "quiet", + "-print_format", + "json", + "-show_format", + "-show_streams", + url, + ] + try: + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) - await p.wait() + stdout, _ = await proc.communicate() + if proc.returncode != 0: + return False, 0, 0, None, -1 - if p.returncode == 0: - result = (await p.stdout.read()).decode() - else: - result = None + 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 - if result: - try: - duration = float(result.strip()) - except ValueError: - duration = -1 - else: - duration = -1 + v = video_streams[0] + width = int(v.get("width", 0)) + height = int(v.get("height", 0)) + codec = v.get("codec_name") - return duration + duration_str = data.get("format", {}).get("duration") + duration = float(duration_str) if duration_str else -1.0 - width = height = 0 + return True, width, height, codec, duration + except (json.JSONDecodeError, ValueError, KeyError, asyncio.SubprocessError): + return False, 0, 0, None, -1 - try: - # Open the video stream using OpenCV - video = cv2.VideoCapture(url) + def probe_with_cv2(url: str) -> tuple[bool, int, int, Optional[str], 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 - # Check if the video stream was opened successfully - if not video.isOpened(): - video = None - except Exception: - video = None + 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 - result = {} + 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 + + # try cv2 first + 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) + + 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"] = await calculate_duration(video) - - if video is not None: - # Get the width of frames in the video stream - width = video.get(cv2.CAP_PROP_FRAME_WIDTH) - - # Get the height of frames in the video stream - height = video.get(cv2.CAP_PROP_FRAME_HEIGHT) - - # Get the stream encoding - fourcc_int = int(video.get(cv2.CAP_PROP_FOURCC)) - fourcc = ( - chr((fourcc_int >> 0) & 255) - + chr((fourcc_int >> 8) & 255) - + chr((fourcc_int >> 16) & 255) - + chr((fourcc_int >> 24) & 255) - ) - - # Release the video stream - video.release() - - result["width"] = round(width) - result["height"] = round(height) - result["fourcc"] = fourcc + result["duration"] = duration return result @@ -768,3 +826,65 @@ def set_file_limit() -> None: 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 000000000..1e7b49c24 --- /dev/null +++ b/frigate/util/time.py @@ -0,0 +1,100 @@ +"""Time utilities.""" + +import datetime +import logging +from typing import Tuple +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.timezone.utc) + datetime.timedelta( + days=1 + ) + logger.warning( + "Using utc for maintenance due to missing or incorrect timezone set" + ) + + return tomorrow.replace(hour=hour, minute=0, second=0).astimezone( + datetime.timezone.utc + ) + + +def is_current_hour(timestamp: int) -> bool: + """Returns if timestamp is in the current UTC hour.""" + start_of_next_hour = ( + datetime.datetime.now(datetime.timezone.utc).replace( + minute=0, second=0, microsecond=0 + ) + + datetime.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/video.py b/frigate/video.py index f2197ed66..615c61d61 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -1,27 +1,29 @@ -import datetime import logging -import multiprocessing as mp -import os import queue -import signal import subprocess as sp import threading import time +from datetime import datetime, timedelta, timezone from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent from typing import Any import cv2 -from setproctitle import setproctitle from frigate.camera import CameraMetrics, PTZMetrics -from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.inter_process import InterProcessRequestor -from frigate.config import CameraConfig, DetectConfig, ModelConfig +from frigate.comms.recordings_updater import ( + RecordingsDataSubscriber, + RecordingsDataTypeEnum, +) +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 ( - CACHE_DIR, - CACHE_SEGMENT_FORMAT, + PROCESS_PRIORITY_HIGH, REQUEST_REGION_GRID, ) from frigate.log import LogPipe @@ -32,7 +34,7 @@ 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, get_tomorrow_at_time +from frigate.util.builtin import EventsPerSecond from frigate.util.image import ( FrameManager, SharedMemoryFrameManager, @@ -50,27 +52,30 @@ from frigate.util.object import ( is_object_filtered, reduce_detections, ) -from frigate.util.services import listen +from frigate.util.process import FrigateProcess +from frigate.util.time import get_tomorrow_at_time logger = logging.getLogger(__name__) -def stop_ffmpeg(ffmpeg_process, logger): +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) @@ -95,7 +100,7 @@ def start_or_restart_ffmpeg( def capture_frames( - ffmpeg_process, + ffmpeg_process: sp.Popen[Any], config: CameraConfig, shm_frame_count: int, frame_index: int, @@ -106,68 +111,70 @@ def capture_frames( 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 = ConfigSubscriber(f"config/enabled/{config.name}", True) + config_subscriber = CameraConfigUpdateSubscriber( + None, {config.name: config}, [CameraConfigUpdateEnum.enabled] + ) def get_enabled_state(): """Fetch the latest enabled state from ZMQ.""" - _, config_data = config_subscriber.check_for_update() - - if config_data: - config.enabled = config_data.enabled - + config_subscriber.check_for_updates() return config.enabled - 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.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(): + try: + while not stop_event.is_set(): + if not get_enabled_state(): + logger.debug(f"Stopping capture thread for disabled {config.name}") break - logger.error(f"{config.name}: Unable to read frames from ffmpeg process.") + 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 - if ffmpeg_process.poll() is not None: logger.error( - f"{config.name}: ffmpeg process is not running. exiting capture thread..." + f"{config.name}: Unable to read frames from ffmpeg process." ) - break - continue + if ffmpeg_process.poll() is not None: + logger.error( + f"{config.name}: ffmpeg process is not running. exiting capture thread..." + ) + break - frame_rate.update() + continue - # 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_rate.update() - frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1 + # 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, - camera_name, config: CameraConfig, shm_frame_count: int, frame_queue: Queue, @@ -177,13 +184,12 @@ class CameraWatchdog(threading.Thread): stop_event, ): threading.Thread.__init__(self) - self.logger = logging.getLogger(f"watchdog.{camera_name}") - self.camera_name = camera_name + 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.camera_name}.detect") + 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 @@ -196,16 +202,22 @@ class CameraWatchdog(threading.Thread): self.stop_event = stop_event self.sleeptime = self.config.ffmpeg.retry_interval - self.config_subscriber = ConfigSubscriber(f"config/enabled/{camera_name}", True) + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {config.name: config}, + [CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record], + ) + self.requestor = InterProcessRequestor() self.was_enabled = self.config.enabled + 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 + def _update_enabled_state(self) -> bool: """Fetch the latest config and update enabled state.""" - _, config_data = self.config_subscriber.check_for_update() - if config_data: - self.config.enabled = config_data.enabled - return config_data.enabled - + self.config_subscriber.check_for_updates() return self.config.enabled def reset_capture_thread( @@ -229,6 +241,16 @@ class CameraWatchdog(threading.Thread): else: self.ffmpeg_detect_process.wait() + # 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." ) @@ -245,61 +267,147 @@ class CameraWatchdog(threading.Thread): enabled = self._update_enabled_state() if enabled != self.was_enabled: if enabled: - self.logger.debug(f"Enabling camera {self.camera_name}") + self.logger.debug(f"Enabling camera {self.config.name}") self.start_all_ffmpeg() + + # reset all timestamps + self.latest_valid_segment_time = 0 + self.latest_invalid_segment_time = 0 + self.latest_cache_segment_time = 0 else: - self.logger.debug(f"Disabling camera {self.camera_name}") + self.logger.debug(f"Disabling camera {self.config.name}") self.stop_all_ffmpeg() + + # update camera status + self.requestor.send_data( + f"{self.config.name}/status/detect", "disabled" + ) + self.requestor.send_data( + f"{self.config.name}/status/record", "disabled" + ) self.was_enabled = enabled continue if not enabled: continue - now = datetime.datetime.now().timestamp() + 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.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.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.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() if not self.capture_thread.is_alive(): + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") self.camera_fps.value = 0 self.logger.error( - f"Ffmpeg process crashed unexpectedly for {self.camera_name}." + f"Ffmpeg process crashed unexpectedly for {self.config.name}." ) self.reset_capture_thread(terminate=False) elif self.camera_fps.value >= (self.config.detect.fps + 10): self.fps_overflow_count += 1 if self.fps_overflow_count == 3: + self.requestor.send_data( + f"{self.config.name}/status/detect", "offline" + ) self.fps_overflow_count = 0 self.camera_fps.value = 0 self.logger.info( - f"{self.camera_name} exceeded fps limit. Exiting ffmpeg..." + f"{self.config.name} exceeded fps limit. Exiting ffmpeg..." ) self.reset_capture_thread(drain_output=False) elif now - self.capture_thread.current_frame.value > 20: + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") self.camera_fps.value = 0 self.logger.info( - f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg..." + f"No frames received from {self.config.name} in 20 seconds. Exiting ffmpeg..." ) self.reset_capture_thread() else: # process is running normally + self.requestor.send_data(f"{self.config.name}/status/detect", "online") 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"]: - latest_segment_time = self.get_latest_segment_datetime( - p.get( - "latest_segment_time", - datetime.datetime.now().astimezone(datetime.timezone.utc), + now_utc = datetime.now().astimezone(timezone.utc) + + latest_cache_dt = ( + datetime.fromtimestamp( + self.latest_cache_segment_time, tz=timezone.utc ) + if self.latest_cache_segment_time > 0 + else now_utc - timedelta(seconds=1) ) - if datetime.datetime.now().astimezone(datetime.timezone.utc) > ( - latest_segment_time + datetime.timedelta(seconds=120) - ): + latest_valid_dt = ( + datetime.fromtimestamp( + self.latest_valid_segment_time, tz=timezone.utc + ) + if self.latest_valid_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + latest_invalid_dt = ( + datetime.fromtimestamp( + self.latest_invalid_segment_time, tz=timezone.utc + ) + 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 + cache_stale = now_utc > (latest_cache_dt + timedelta(seconds=120)) + valid_stale = now_utc > (latest_valid_dt + timedelta(seconds=120)) + invalid_stale_condition = ( + self.latest_invalid_segment_time > 0 + and now_utc > (latest_invalid_dt + timedelta(seconds=120)) + 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"No new recording segments were created for {self.camera_name} in the last 120s. restarting the ffmpeg record process..." + f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..." ) p["process"] = start_or_restart_ffmpeg( p["cmd"], @@ -307,13 +415,27 @@ class CameraWatchdog(threading.Thread): p["logpipe"], ffmpeg_process=p["process"], ) + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role}", "offline" + ) + continue else: - p["latest_segment_time"] = latest_segment_time + self.requestor.send_data( + f"{self.config.name}/status/record", "online" + ) + 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}", "offline" + ) + p["logpipe"].dump() p["process"] = start_or_restart_ffmpeg( p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"] @@ -322,6 +444,7 @@ class CameraWatchdog(threading.Thread): self.stop_all_ffmpeg() self.logpipe.close() self.config_subscriber.stop() + self.segment_subscriber.stop() def start_ffmpeg_detect(self): ffmpeg_cmd = [ @@ -331,7 +454,7 @@ class CameraWatchdog(threading.Thread): ffmpeg_cmd, self.logger, self.logpipe, self.frame_size ) self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid - self.capture_thread = CameraCapture( + self.capture_thread = CameraCaptureRunner( self.config, self.shm_frame_count, self.frame_index, @@ -346,13 +469,13 @@ class CameraWatchdog(threading.Thread): def start_all_ffmpeg(self): """Start all ffmpeg processes (detection and others).""" - logger.debug(f"Starting all ffmpeg processes for {self.camera_name}") + 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.camera_name}.{'_'.join(sorted(c['roles']))}" + f"ffmpeg.{self.config.name}.{'_'.join(sorted(c['roles']))}" ) self.ffmpeg_other_processes.append( { @@ -365,12 +488,12 @@ class CameraWatchdog(threading.Thread): def stop_all_ffmpeg(self): """Stop all ffmpeg processes (detection and others).""" - logger.debug(f"Stopping all ffmpeg processes for {self.camera_name}") + 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.camera_name} did not stop gracefully." + 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) @@ -381,35 +504,8 @@ class CameraWatchdog(threading.Thread): p["logpipe"].close() self.ffmpeg_other_processes.clear() - def get_latest_segment_datetime( - self, latest_segment: datetime.datetime - ) -> datetime.datetime: - """Checks if ffmpeg is still writing recording segments to cache.""" - cache_files = sorted( - [ - 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_") - ] - ) - newest_segment_time = latest_segment - for file in cache_files: - if self.camera_name in file: - basename = os.path.splitext(file)[0] - _, date = basename.rsplit("@", maxsplit=1) - segment_time = datetime.datetime.strptime( - date, CACHE_SEGMENT_FORMAT - ).astimezone(datetime.timezone.utc) - if segment_time > newest_segment_time: - newest_segment_time = segment_time - - return newest_segment_time - - -class CameraCapture(threading.Thread): +class CameraCaptureRunner(threading.Thread): def __init__( self, config: CameraConfig, @@ -453,110 +549,122 @@ class CameraCapture(threading.Thread): ) -def capture_camera( - name, config: CameraConfig, shm_frame_count: int, camera_metrics: CameraMetrics -): - stop_event = mp.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 receiveSignal(signalNumber, frame): - stop_event.set() - - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) - - threading.current_thread().name = f"capture:{name}" - setproctitle(f"frigate.capture:{name}") - - camera_watchdog = CameraWatchdog( - name, - config, - shm_frame_count, - camera_metrics.frame_queue, - camera_metrics.camera_fps, - camera_metrics.skipped_fps, - camera_metrics.ffmpeg_pid, - stop_event, - ) - camera_watchdog.start() - camera_watchdog.join() + 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.stop_event, + ) + camera_watchdog.start() + camera_watchdog.join() -def track_camera( - name, - config: CameraConfig, - model_config: ModelConfig, - labelmap: dict[int, str], - detection_queue: Queue, - result_connection: MpEvent, - detected_objects_queue, - camera_metrics: CameraMetrics, - ptz_metrics: PTZMetrics, - region_grid: list[list[dict[str, Any]]], -): - stop_event = mp.Event() - - def receiveSignal(signalNumber, frame): - stop_event.set() - - signal.signal(signal.SIGTERM, receiveSignal) - signal.signal(signal.SIGINT, receiveSignal) - - threading.current_thread().name = f"process:{name}" - setproctitle(f"frigate.process:{name}") - listen() - - frame_queue = camera_metrics.frame_queue - - frame_shape = config.frame_shape - objects_to_track = config.objects.track - object_filters = config.objects.filters - - motion_detector = ImprovedMotionDetector( - frame_shape, - config.motion, - config.detect.fps, - name=config.name, - ptz_metrics=ptz_metrics, - ) - object_detector = RemoteObjectDetector( - name, labelmap, detection_queue, result_connection, model_config, stop_event - ) - - object_tracker = NorfairTracker(config, ptz_metrics) - - frame_manager = SharedMemoryFrameManager() - - # create communication for region grid updates - requestor = InterProcessRequestor() - - process_frames( - name, - requestor, - frame_queue, - frame_shape, - model_config, - config, - config.detect, - frame_manager, - motion_detector, - object_detector, - object_tracker, +class CameraTracker(FrigateProcess): + def __init__( + self, + config: CameraConfig, + model_config: ModelConfig, + labelmap: dict[int, str], + detection_queue: Queue, detected_objects_queue, - camera_metrics, - objects_to_track, - object_filters, - stop_event, - ptz_metrics, - region_grid, - ) + 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 - # empty the frame queue - logger.info(f"{name}: emptying frame queue") - while not frame_queue.empty(): - (frame_name, _) = frame_queue.get(False) - frame_manager.delete(frame_name) + def run(self) -> None: + self.pre_run_setup(self.log_config) + frame_queue = self.camera_metrics.frame_queue + frame_shape = self.config.frame_shape - logger.info(f"{name}: exiting subprocess") + 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( @@ -597,29 +705,33 @@ def detect( def process_frames( - camera_name: str, requestor: InterProcessRequestor, frame_queue: Queue, frame_shape: tuple[int, int], model_config: ModelConfig, camera_config: CameraConfig, - detect_config: DetectConfig, frame_manager: FrameManager, motion_detector: MotionDetector, object_detector: RemoteObjectDetector, object_tracker: ObjectTracker, detected_objects_queue: Queue, camera_metrics: CameraMetrics, - objects_to_track: list[str], - object_filters, 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) - detect_config_subscriber = ConfigSubscriber(f"config/detect/{camera_name}", True) - enabled_config_subscriber = ConfigSubscriber(f"config/enabled/{camera_name}", True) + config_subscriber = CameraConfigUpdateSubscriber( + None, + {camera_config.name: camera_config}, + [ + CameraConfigUpdateEnum.detect, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + ], + ) fps_tracker = EventsPerSecond() fps_tracker.start() @@ -654,18 +766,24 @@ def process_frames( ] while not stop_event.is_set(): - _, updated_enabled_config = enabled_config_subscriber.check_for_update() + updated_configs = config_subscriber.check_for_updates() - if updated_enabled_config: + if "enabled" in updated_configs: prev_enabled = camera_enabled - camera_enabled = updated_enabled_config.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_name} disabled, clearing tracked objects") + logger.debug( + f"Camera {camera_config.name} disabled, clearing tracked objects" + ) prev_enabled = camera_enabled # Clear norfair's dictionaries @@ -686,17 +804,8 @@ def process_frames( time.sleep(0.1) continue - # check for updated detect config - _, updated_detect_config = detect_config_subscriber.check_for_update() - - if updated_detect_config: - detect_config = updated_detect_config - - if ( - datetime.datetime.now().astimezone(datetime.timezone.utc) - > next_region_update - ): - region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_name) + if datetime.now().astimezone(timezone.utc) > next_region_update: + region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name) next_region_update = get_tomorrow_at_time(2) try: @@ -716,7 +825,9 @@ def process_frames( frame = frame_manager.get(frame_name, (frame_shape[0] * 3 // 2, frame_shape[1])) if frame is None: - logger.debug(f"{camera_name}: frame {frame_time} is not in memory store.") + logger.debug( + f"{camera_config.name}: frame {frame_time} is not in memory store." + ) continue # look for motion if enabled @@ -726,14 +837,14 @@ def process_frames( consolidated_detections = [] # if detection is disabled - if not detect_config.enabled: + 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 == detect_config.stationary.interval: + if stationary_frame_counter == camera_config.detect.stationary.interval: stationary_frame_counter = 0 stationary_object_ids = [] else: @@ -742,7 +853,8 @@ def process_frames( obj["id"] for obj in object_tracker.tracked_objects.values() # if it has exceeded the stationary threshold - if obj["motionless_count"] >= detect_config.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 @@ -757,7 +869,8 @@ def process_frames( ( # use existing object box for stationary objects obj["estimate"] - if obj["motionless_count"] < detect_config.stationary.threshold + if obj["motionless_count"] + < camera_config.detect.stationary.threshold else obj["box"] ) for obj in object_tracker.tracked_objects.values() @@ -831,13 +944,13 @@ def process_frames( for region in regions: detections.extend( detect( - detect_config, + camera_config.detect, object_detector, frame, model_config, region, - objects_to_track, - object_filters, + camera_config.objects.track, + camera_config.objects.filters, ) ) @@ -953,7 +1066,7 @@ def process_frames( ) cv2.imwrite( - f"debug/frames/{camera_name}-{'{:.6f}'.format(frame_time)}.jpg", + f"debug/frames/{camera_config.name}-{'{:.6f}'.format(frame_time)}.jpg", bgr_frame, ) # add to the queue if not full @@ -965,7 +1078,7 @@ def process_frames( camera_metrics.process_fps.value = fps_tracker.eps() detected_objects_queue.put( ( - camera_name, + camera_config.name, frame_name, frame_time, detections, @@ -978,5 +1091,4 @@ def process_frames( motion_detector.stop() requestor.stop() - detect_config_subscriber.stop() - enabled_config_subscriber.stop() + config_subscriber.stop() diff --git a/generate_config_translations.py b/generate_config_translations.py new file mode 100644 index 000000000..c19578f1a --- /dev/null +++ b/generate_config_translations.py @@ -0,0 +1,163 @@ +#!/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 shutil +from pathlib import Path +from typing import Any, Dict, Optional, get_args, get_origin + +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from frigate.config.config import FrigateConfig + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def get_field_translations(field_info: FieldInfo) -> 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 process_model_fields(model: type[BaseModel]) -> Dict[str, Any]: + """ + Recursively process a Pydantic model to extract translations. + + Returns a nested dictionary structure matching the config schema, + with title and description for each field. + """ + translations = {} + + model_fields = model.model_fields + + for field_name, field_info in model_fields.items(): + field_translations = get_field_translations(field_info) + + # Get the field's type annotation + field_type = field_info.annotation + + # Handle Optional types + origin = get_origin(field_type) + + if origin is Optional or ( + hasattr(origin, "__name__") and origin.__name__ == "UnionType" + ): + args = get_args(field_type) + field_type = next( + (arg for arg in args if arg is not type(None)), field_type + ) + + # Handle Dict types (like Dict[str, CameraConfig]) + if get_origin(field_type) is dict: + dict_args = get_args(field_type) + + if len(dict_args) >= 2: + value_type = dict_args[1] + + if isinstance(value_type, type) and issubclass(value_type, BaseModel): + nested_translations = process_model_fields(value_type) + + if nested_translations: + field_translations["properties"] = nested_translations + elif isinstance(field_type, type) and issubclass(field_type, BaseModel): + nested_translations = process_model_fields(field_type) + if nested_translations: + field_translations["properties"] = nested_translations + + if field_translations: + translations[field_name] = field_translations + + return translations + + +def generate_section_translation( + section_name: str, field_info: FieldInfo +) -> Dict[str, Any]: + """ + Generate translation structure for a top-level config section. + """ + section_translations = get_field_translations(field_info) + field_type = field_info.annotation + origin = get_origin(field_type) + + if origin is Optional or ( + hasattr(origin, "__name__") and origin.__name__ == "UnionType" + ): + args = get_args(field_type) + field_type = next((arg for arg in args if arg is not type(None)), field_type) + + # Handle Dict types (like detectors, cameras, camera_groups) + if get_origin(field_type) is dict: + dict_args = get_args(field_type) + if len(dict_args) >= 2: + value_type = dict_args[1] + if isinstance(value_type, type) and issubclass(value_type, BaseModel): + nested = process_model_fields(value_type) + if nested: + section_translations["properties"] = nested + + # If the field itself is a BaseModel, process it + elif isinstance(field_type, type) and issubclass(field_type, BaseModel): + nested = process_model_fields(field_type) + if nested: + section_translations["properties"] = nested + + return section_translations + + +def main(): + """Main function to generate config translations.""" + + # Define output directory + output_dir = Path(__file__).parent / "web" / "public" / "locales" / "en" / "config" + + logger.info(f"Output directory: {output_dir}") + + # Clean and recreate the output directory + if output_dir.exists(): + logger.info(f"Removing existing directory: {output_dir}") + shutil.rmtree(output_dir) + + logger.info(f"Creating directory: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + + config_fields = FrigateConfig.model_fields + logger.info(f"Found {len(config_fields)} top-level config sections") + + for field_name, field_info in config_fields.items(): + if field_name.startswith("_"): + continue + + logger.info(f"Processing section: {field_name}") + section_data = generate_section_translation(field_name, field_info) + + if not section_data: + logger.warning(f"No translations found for section: {field_name}") + continue + + output_file = output_dir / f"{field_name}.json" + with open(output_file, "w", encoding="utf-8") as f: + json.dump(section_data, f, indent=2, ensure_ascii=False) + + logger.info(f"Generated: {output_file}") + + logger.info("Translation generation complete!") + + +if __name__ == "__main__": + main() diff --git a/migrations/001_create_events_table.py b/migrations/001_create_events_table.py index 9e8ad1b60..57f9aa678 100644 --- a/migrations/001_create_events_table.py +++ b/migrations/001_create_events_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/002_add_clip_snapshot.py b/migrations/002_add_clip_snapshot.py index 1431c9c85..47a46f572 100644 --- a/migrations/002_add_clip_snapshot.py +++ b/migrations/002_add_clip_snapshot.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/003_create_recordings_table.py b/migrations/003_create_recordings_table.py index 77f9827cf..3956ae929 100644 --- a/migrations/003_create_recordings_table.py +++ b/migrations/003_create_recordings_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/004_add_bbox_region_area.py b/migrations/004_add_bbox_region_area.py index da4ca7ac8..a1aa35aab 100644 --- a/migrations/004_add_bbox_region_area.py +++ b/migrations/004_add_bbox_region_area.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/005_make_end_time_nullable.py b/migrations/005_make_end_time_nullable.py index 87d0e3fd4..d80d31d88 100644 --- a/migrations/005_make_end_time_nullable.py +++ b/migrations/005_make_end_time_nullable.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/006_add_motion_active_objects.py b/migrations/006_add_motion_active_objects.py index 6ab67ee3a..2fe1f908a 100644 --- a/migrations/006_add_motion_active_objects.py +++ b/migrations/006_add_motion_active_objects.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/007_add_retain_indefinitely.py b/migrations/007_add_retain_indefinitely.py index cb5f9da92..e5d07ab7a 100644 --- a/migrations/007_add_retain_indefinitely.py +++ b/migrations/007_add_retain_indefinitely.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/008_add_sub_label.py b/migrations/008_add_sub_label.py index 56c4bb75a..bba38343a 100644 --- a/migrations/008_add_sub_label.py +++ b/migrations/008_add_sub_label.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/009_add_object_filter_ratio.py b/migrations/009_add_object_filter_ratio.py index e5a00683d..77a25ebab 100644 --- a/migrations/009_add_object_filter_ratio.py +++ b/migrations/009_add_object_filter_ratio.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/010_add_plus_image_id.py b/migrations/010_add_plus_image_id.py index 6b8c7ccc6..d403dbb71 100644 --- a/migrations/010_add_plus_image_id.py +++ b/migrations/010_add_plus_image_id.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/011_update_indexes.py b/migrations/011_update_indexes.py index 5c13baa54..6d411d3df 100644 --- a/migrations/011_update_indexes.py +++ b/migrations/011_update_indexes.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/012_add_segment_size.py b/migrations/012_add_segment_size.py index 7a1c79736..8ea91a126 100644 --- a/migrations/012_add_segment_size.py +++ b/migrations/012_add_segment_size.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/013_create_timeline_table.py b/migrations/013_create_timeline_table.py index 7a83d96c7..9ed260621 100644 --- a/migrations/013_create_timeline_table.py +++ b/migrations/013_create_timeline_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/014_event_updates_for_fp.py b/migrations/014_event_updates_for_fp.py index caa609bfa..f44f6c93b 100644 --- a/migrations/014_event_updates_for_fp.py +++ b/migrations/014_event_updates_for_fp.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/015_event_refactor.py b/migrations/015_event_refactor.py index 1bcb9c510..92d8a165e 100644 --- a/migrations/015_event_refactor.py +++ b/migrations/015_event_refactor.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/016_sublabel_increase.py b/migrations/016_sublabel_increase.py index 536ea0a61..66411ffae 100644 --- a/migrations/016_sublabel_increase.py +++ b/migrations/016_sublabel_increase.py @@ -4,8 +4,8 @@ from frigate.models import Event def migrate(migrator, database, fake=False, **kwargs): - migrator.change_columns(Event, sub_label=pw.CharField(max_length=100, null=True)) + migrator.change_fields(Event, sub_label=pw.CharField(max_length=100, null=True)) def rollback(migrator, database, fake=False, **kwargs): - migrator.change_columns(Event, sub_label=pw.CharField(max_length=20, null=True)) + 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 index 66d1fcc6a..63685eaf7 100644 --- a/migrations/017_update_indexes.py +++ b/migrations/017_update_indexes.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/018_add_dbfs.py b/migrations/018_add_dbfs.py index 5b5c56b9d..485e954e3 100644 --- a/migrations/018_add_dbfs.py +++ b/migrations/018_add_dbfs.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/019_create_regions_table.py b/migrations/019_create_regions_table.py index 2900b78d2..961aaf81d 100644 --- a/migrations/019_create_regions_table.py +++ b/migrations/019_create_regions_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/020_update_index_recordings.py b/migrations/020_update_index_recordings.py index 7d0c2b860..d6af71c7c 100644 --- a/migrations/020_update_index_recordings.py +++ b/migrations/020_update_index_recordings.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/021_create_previews_table.py b/migrations/021_create_previews_table.py index 1036e7cdd..b77536099 100644 --- a/migrations/021_create_previews_table.py +++ b/migrations/021_create_previews_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/022_create_review_segment_table.py b/migrations/022_create_review_segment_table.py index 681795e37..91d0c8c6b 100644 --- a/migrations/022_create_review_segment_table.py +++ b/migrations/022_create_review_segment_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/023_add_regions.py b/migrations/023_add_regions.py index 17d93962a..7649baa14 100644 --- a/migrations/023_add_regions.py +++ b/migrations/023_add_regions.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/024_create_export_table.py b/migrations/024_create_export_table.py index 414bd712e..8de2f17d4 100644 --- a/migrations/024_create_export_table.py +++ b/migrations/024_create_export_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/025_create_user_table.py b/migrations/025_create_user_table.py index 6b971a6f1..dec57d66f 100644 --- a/migrations/025_create_user_table.py +++ b/migrations/025_create_user_table.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/026_add_notification_tokens.py b/migrations/026_add_notification_tokens.py index 37506c406..23860c58f 100644 --- a/migrations/026_add_notification_tokens.py +++ b/migrations/026_add_notification_tokens.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/027_create_explore_index.py b/migrations/027_create_explore_index.py index 6d0012c6c..f08c0bbc9 100644 --- a/migrations/027_create_explore_index.py +++ b/migrations/027_create_explore_index.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/028_optional_event_thumbnail.py b/migrations/028_optional_event_thumbnail.py index 3e36a28cc..52177004b 100644 --- a/migrations/028_optional_event_thumbnail.py +++ b/migrations/028_optional_event_thumbnail.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/029_add_user_role.py b/migrations/029_add_user_role.py index 484e0c548..e0fb1bb16 100644 --- a/migrations/029_add_user_role.py +++ b/migrations/029_add_user_role.py @@ -5,7 +5,7 @@ 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.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 diff --git a/migrations/030_create_user_review_status.py b/migrations/030_create_user_review_status.py index 17f2b36b9..ddcf063ec 100644 --- a/migrations/030_create_user_review_status.py +++ b/migrations/030_create_user_review_status.py @@ -8,7 +8,7 @@ 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.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 @@ -54,7 +54,9 @@ def migrate(migrator, database, fake=False, **kwargs): # Migrate existing has_been_reviewed data to UserReviewStatus for all users def migrate_data(): - all_users = list(User.select()) + # 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 @@ -63,7 +65,7 @@ def migrate(migrator, database, fake=False, **kwargs): ) reviewed_segment_ids = [row[0] for row in cursor.fetchall()] # also migrate for anonymous (unauthenticated users) - usernames = [user.username for user in all_users] + ["anonymous"] + usernames = [user[0] for user in all_users] + ["anonymous"] for segment_id in reviewed_segment_ids: for username in usernames: @@ -74,7 +76,7 @@ def migrate(migrator, database, fake=False, **kwargs): ) if not fake: # Only run data migration if not faking - migrator.python(migrate_data) + migrator.run(migrate_data) migrator.sql('ALTER TABLE "reviewsegment" DROP COLUMN "has_been_reviewed"') diff --git a/migrations/031_create_trigger_table.py b/migrations/031_create_trigger_table.py new file mode 100644 index 000000000..c2ac2e026 --- /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 000000000..5382c12e2 --- /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/notebooks/YOLO_NAS_Pretrained_Export.ipynb b/notebooks/YOLO_NAS_Pretrained_Export.ipynb index 4e0439e9e..e9ee22314 100644 --- a/notebooks/YOLO_NAS_Pretrained_Export.ipynb +++ b/notebooks/YOLO_NAS_Pretrained_Export.ipynb @@ -19,8 +19,8 @@ }, "outputs": [], "source": [ - "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.11/dist-packages/super_gradients/training/pretrained_models.py\n", - "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.11/dist-packages/super_gradients/training/utils/checkpoint_utils.py" + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/pretrained_models.py\n", + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/utils/checkpoint_utils.py" ] }, { diff --git a/web/.gitignore b/web/.gitignore index a547bf36d..1cac5597e 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env \ No newline at end of file diff --git a/web/components.json b/web/components.json index 3f112537b..679fbd7af 100644 --- a/web/components.json +++ b/web/components.json @@ -4,8 +4,8 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", - "css": "index.css", + "config": "tailwind.config.cjs", + "css": "src/index.css", "baseColor": "slate", "cssVariables": true }, diff --git a/web/images/branding/LICENSE b/web/images/branding/LICENSE new file mode 100644 index 000000000..6dbfbe644 --- /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/apple-touch-icon.png b/web/images/branding/apple-touch-icon.png similarity index 100% rename from web/images/apple-touch-icon.png rename to web/images/branding/apple-touch-icon.png diff --git a/web/images/favicon-16x16.png b/web/images/branding/favicon-16x16.png similarity index 100% rename from web/images/favicon-16x16.png rename to web/images/branding/favicon-16x16.png diff --git a/web/images/favicon-32x32.png b/web/images/branding/favicon-32x32.png similarity index 100% rename from web/images/favicon-32x32.png rename to web/images/branding/favicon-32x32.png diff --git a/web/images/favicon.ico b/web/images/branding/favicon.ico similarity index 100% rename from web/images/favicon.ico rename to web/images/branding/favicon.ico diff --git a/web/images/favicon.png b/web/images/branding/favicon.png similarity index 100% rename from web/images/favicon.png rename to web/images/branding/favicon.png diff --git a/web/images/favicon.svg b/web/images/branding/favicon.svg similarity index 100% rename from web/images/favicon.svg rename to web/images/branding/favicon.svg diff --git a/web/images/mstile-150x150.png b/web/images/branding/mstile-150x150.png similarity index 100% rename from web/images/mstile-150x150.png rename to web/images/branding/mstile-150x150.png diff --git a/web/index.html b/web/index.html index 0e361fb5a..0805deca3 100644 --- a/web/index.html +++ b/web/index.html @@ -2,29 +2,29 @@ - + Frigate - + - + diff --git a/web/login.html b/web/login.html index 39ca78c3c..fc0fb551e 100644 --- a/web/login.html +++ b/web/login.html @@ -2,29 +2,29 @@ - + Frigate - + - + diff --git a/web/package-lock.json b/web/package-lock.json index 5d4a4e106..2fd083427 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,8 +14,9 @@ "@radix-ui/react-alert-dialog": "^1.1.6", "@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.6", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", @@ -23,14 +24,14 @@ "@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.2", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@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.1.8", + "@radix-ui/react-tooltip": "^1.2.8", "apexcharts": "^3.52.0", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", @@ -63,7 +64,7 @@ "react-i18next": "^15.2.0", "react-icons": "^5.5.0", "react-konva": "^18.2.10", - "react-router-dom": "^6.26.0", + "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", "react-tracked": "^2.0.1", "react-transition-group": "^4.4.5", @@ -115,7 +116,7 @@ "prettier-plugin-tailwindcss": "^0.6.5", "tailwindcss": "^3.4.9", "typescript": "^5.8.2", - "vite": "^6.2.0", + "vite": "^6.4.1", "vitest": "^3.0.7" } }, @@ -1250,6 +1251,42 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "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-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "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-alert-dialog/node_modules/@radix-ui/react-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", @@ -1344,6 +1381,171 @@ } } }, + "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-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-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", @@ -1447,23 +1649,23 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "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.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", + "@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" }, @@ -1482,14 +1684,255 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "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-compose-refs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "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-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-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/react-compose-refs": "1.1.1" + "@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-dialog/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-dialog/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-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-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-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-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-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-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-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" @@ -2073,12 +2516,35 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", - "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@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-separator/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": "*", @@ -2129,9 +2595,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -2275,23 +2741,23 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", - "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "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.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.2", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.2" + "@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": "*", @@ -2308,13 +2774,99 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "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-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-compose-refs": "1.1.1" + "@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-tooltip/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-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-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-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": "*", @@ -2326,6 +2878,241 @@ } } }, + "node_modules/@radix-ui/react-tooltip/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-tooltip/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-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-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-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-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-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-tooltip/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-tooltip/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-tooltip/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-tooltip/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/@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", @@ -2359,6 +3146,39 @@ } } }, + "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.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", @@ -2473,9 +3293,9 @@ "license": "MIT" }, "node_modules/@remix-run/router": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz", - "integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==", + "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" @@ -3863,6 +4683,19 @@ "node": ">=8" } }, + "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3882,9 +4715,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "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": [ { @@ -4799,6 +5632,20 @@ "csstype": "^3.0.2" } }, + "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", @@ -4859,6 +5706,24 @@ "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", @@ -4866,6 +5731,33 @@ "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", @@ -5402,12 +6294,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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": { @@ -5487,6 +6382,30 @@ "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-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5496,6 +6415,19 @@ "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/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5564,6 +6496,18 @@ "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/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5593,10 +6537,38 @@ "node": ">=8" } }, + "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.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "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" }, @@ -6320,6 +7292,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7636,12 +8617,12 @@ } }, "node_modules/react-router": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz", - "integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==", + "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.19.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -7651,13 +8632,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz", - "integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==", + "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.19.0", - "react-router": "6.26.0" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -8682,6 +9663,54 @@ "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", @@ -9048,15 +10077,18 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -9150,6 +10182,37 @@ "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", diff --git a/web/package.json b/web/package.json index 907960cc7..546c22f31 100644 --- a/web/package.json +++ b/web/package.json @@ -20,8 +20,9 @@ "@radix-ui/react-alert-dialog": "^1.1.6", "@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.6", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", @@ -29,14 +30,14 @@ "@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.2", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@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.1.8", + "@radix-ui/react-tooltip": "^1.2.8", "apexcharts": "^3.52.0", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", @@ -69,7 +70,7 @@ "react-i18next": "^15.2.0", "react-icons": "^5.5.0", "react-konva": "^18.2.10", - "react-router-dom": "^6.26.0", + "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", "react-tracked": "^2.0.1", "react-transition-group": "^4.4.5", @@ -121,7 +122,7 @@ "prettier-plugin-tailwindcss": "^0.6.5", "tailwindcss": "^3.4.9", "typescript": "^5.8.2", - "vite": "^6.2.0", + "vite": "^6.4.1", "vitest": "^3.0.7" } } diff --git a/web/public/locales/ab/views/classificationModel.json b/web/public/locales/ab/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ab/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ar/audio.json b/web/public/locales/ar/audio.json index 5c6d14263..b72a52c90 100644 --- a/web/public/locales/ar/audio.json +++ b/web/public/locales/ar/audio.json @@ -70,5 +70,9 @@ "clip_clop": "حَوَافِر الخَيْل", "car": "سيارة", "motorcycle": "دراجة نارية", - "bicycle": "دراجة هوائية" + "bicycle": "دراجة هوائية", + "bus": "حافلة", + "train": "قطار", + "boat": "زورق", + "bird": "طائر" } diff --git a/web/public/locales/ar/common.json b/web/public/locales/ar/common.json index 691643630..92390a7ff 100644 --- a/web/public/locales/ar/common.json +++ b/web/public/locales/ar/common.json @@ -3,6 +3,18 @@ "untilForTime": "حتى {{time}}", "untilForRestart": "حتى يعاد تشغيل فرايجيت.", "untilRestart": "حتى إعادة التشغيل", - "ago": "منذ {{timeAgo}}" + "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 index 7ee15b6e2..1c8eabf5f 100644 --- a/web/public/locales/ar/components/auth.json +++ b/web/public/locales/ar/components/auth.json @@ -4,7 +4,12 @@ "user": "أسم المستخدم", "login": "تسجيل الدخول", "errors": { - "usernameRequired": "اسم المستخدم مطلوب" + "usernameRequired": "اسم المستخدم مطلوب", + "passwordRequired": "كلمة المرور مطلوبة", + "rateLimit": "تجاوز الحد الأقصى للمعدل. حاول مرة أخرى في وقت لاحق.", + "webUnknownError": "خطأ غير معروف. تحقق من سجلات وحدة التحكم.", + "loginFailed": "فشل تسجيل الدخول", + "unknownError": "خطأ غير معروف. تحقق من السجلات." } } } diff --git a/web/public/locales/ar/components/camera.json b/web/public/locales/ar/components/camera.json index daaddbfac..9bc19f109 100644 --- a/web/public/locales/ar/components/camera.json +++ b/web/public/locales/ar/components/camera.json @@ -4,7 +4,48 @@ "add": "إضافة مجموعة الكاميرات", "edit": "تعديل مجموعة الكاميرات", "delete": { - "label": "حذف مجموعة الكاميرات" + "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 index 2d3372caa..42918739f 100644 --- a/web/public/locales/ar/components/dialog.json +++ b/web/public/locales/ar/components/dialog.json @@ -3,7 +3,36 @@ "title": "هل أنت متأكد أنك تريد إعادة تشغيل فرايجيت؟", "button": "إعادة التشغيل", "restarting": { - "title": "يتم إعادة تشغيل فرايجيت" + "title": "يتم إعادة تشغيل فرايجيت", + "content": "العد التنازلي", + "button": "فرض إعادة التحميل الآن" + } + }, + "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 index e55bedd66..954d69fac 100644 --- a/web/public/locales/ar/components/filter.json +++ b/web/public/locales/ar/components/filter.json @@ -3,7 +3,29 @@ "labels": { "label": "التسميات", "all": { - "title": "كل التسميات" + "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/player.json b/web/public/locales/ar/components/player.json index da1bd4859..5a3e87d29 100644 --- a/web/public/locales/ar/components/player.json +++ b/web/public/locales/ar/components/player.json @@ -3,6 +3,27 @@ "noPreviewFound": "لا يوجد معاينة", "noPreviewFoundFor": "لا يوجد معاينة لـ{{cameraName}}", "submitFrigatePlus": { - "title": "هل ترغب بإرسال هذه الصوره الى Frigate+؟" + "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/objects.json b/web/public/locales/ar/objects.json index bf0ac8737..4aff9d76e 100644 --- a/web/public/locales/ar/objects.json +++ b/web/public/locales/ar/objects.json @@ -7,5 +7,16 @@ "person": "شخص", "bicycle": "دراجة هوائية", "car": "سيارة", - "motorcycle": "دراجة نارية" + "motorcycle": "دراجة نارية", + "airplane": "طائرة", + "bus": "حافلة", + "traffic_light": "إشارة المرور", + "fire_hydrant": "حنفية إطفاء الحريق", + "street_sign": "لافتة شارع", + "stop_sign": "إشارة توقف", + "parking_meter": "عداد موقف سيارات", + "train": "قطار", + "boat": "زورق", + "bench": "مقعدة", + "bird": "طائر" } diff --git a/web/public/locales/ar/views/classificationModel.json b/web/public/locales/ar/views/classificationModel.json new file mode 100644 index 000000000..bc814e6f5 --- /dev/null +++ b/web/public/locales/ar/views/classificationModel.json @@ -0,0 +1,5 @@ +{ + "train": { + "titleShort": "الأخيرة" + } +} diff --git a/web/public/locales/ar/views/configEditor.json b/web/public/locales/ar/views/configEditor.json index 10e9cd739..6387006ce 100644 --- a/web/public/locales/ar/views/configEditor.json +++ b/web/public/locales/ar/views/configEditor.json @@ -2,5 +2,17 @@ "documentTitle": "محرر الإعدادات - فرايجيت", "configEditor": "محرر الإعدادات", "copyConfig": "نسخ الإعدادات", - "saveAndRestart": "حفظ وإعادة تشغيل" + "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 index 74ec7d7f5..41312c914 100644 --- a/web/public/locales/ar/views/events.json +++ b/web/public/locales/ar/views/events.json @@ -4,5 +4,22 @@ "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 index e430d47d2..4b54ed113 100644 --- a/web/public/locales/ar/views/explore.json +++ b/web/public/locales/ar/views/explore.json @@ -3,6 +3,28 @@ "documentTitle": "اكتشف - فرايجيت", "generativeAI": "ذكاء اصطناعي مولد", "exploreIsUnavailable": { - "title": "المتصفح غير متاح" + "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 index 6d0c418d6..318ec2fd8 100644 --- a/web/public/locales/ar/views/exports.json +++ b/web/public/locales/ar/views/exports.json @@ -1,5 +1,17 @@ { "search": "بحث", "noExports": "لا يوجد تصديرات", - "documentTitle": "التصدير - فرايجيت" + "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 index cb515dde3..5a40c8c59 100644 --- a/web/public/locales/ar/views/faceLibrary.json +++ b/web/public/locales/ar/views/faceLibrary.json @@ -1,10 +1,108 @@ { "description": { - "addFace": "قم بإضافة مجموعة جديدة لمكتبة الأوجه.", + "addFace": "أضف مجموعة جديدة إلى مكتبة الوجوه عن طريق رفع صورتك الأولى.", "invalidName": "أسم غير صالح. يجب أن يشمل الأسم فقط على الحروف، الأرقام، المسافات، الفاصلة العليا، الشرطة التحتية، والشرطة الواصلة.", "placeholder": "أدخل أسم لهذه المجموعة" }, "details": { - "person": "شخص" + "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 index 242365f65..6e4f32d80 100644 --- a/web/public/locales/ar/views/live.json +++ b/web/public/locales/ar/views/live.json @@ -3,6 +3,37 @@ "documentTitle.withCamera": "{{camera}} - بث حي - فرايجيت", "lowBandwidthMode": "وضع موفر للبيانات", "twoWayTalk": { - "enable": "تفعيل المكالمات ثنائية الاتجاه" + "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/recording.json b/web/public/locales/ar/views/recording.json index d79f0ed87..c12dfda01 100644 --- a/web/public/locales/ar/views/recording.json +++ b/web/public/locales/ar/views/recording.json @@ -1,5 +1,12 @@ { "filter": "ترشيح", "export": "إرسال", - "calendar": "التقويم" + "calendar": "التقويم", + "filters": "المنقيات", + "toast": { + "error": { + "noValidTimeSelected": "لم يتم تحديد نطاق زمني صحيح", + "endTimeMustAfterStartTime": "يجب أن يكون وقت الانتهاء بعد وقت بدء التشغيل" + } + } } diff --git a/web/public/locales/ar/views/search.json b/web/public/locales/ar/views/search.json index 3ed3dc2b7..7964a0f0e 100644 --- a/web/public/locales/ar/views/search.json +++ b/web/public/locales/ar/views/search.json @@ -1,5 +1,23 @@ { "search": "بحث", "savedSearches": "عمليات البحث المحفوظة", - "searchFor": "البحث عن {{inputValue}}" + "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 index fb6f81760..6a4065819 100644 --- a/web/public/locales/ar/views/settings.json +++ b/web/public/locales/ar/views/settings.json @@ -2,6 +2,34 @@ "documentTitle": { "camera": "إعدادات الكاميرا - فرايجيت", "default": "الإعدادات - فرايجيت", - "authentication": "إعدادات المصادقة - فرايجيت" + "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 index 581494cb1..e68d544e4 100644 --- a/web/public/locales/ar/views/system.json +++ b/web/public/locales/ar/views/system.json @@ -2,6 +2,79 @@ "documentTitle": { "cameras": "إحصاءات الكاميرات - فرايجيت", "storage": "إحصاءات التخزين - فرايجيت", - "general": "إحصاءات عامة - فرايجيت" + "general": "إحصاءات عامة - فرايجيت", + "enrichments": "إحصاء العمليات", + "logs": { + "frigate": "سجلات Frigate - Frigate", + "go2rtc": "Go2RTC سجلات - Frigate", + "nginx": "سجلات إنجنإكس - Frigate" + } + }, + "metrics": "مقاييس النظام", + "logs": { + "download": { + "label": "تنزيل السجلات" + }, + "copy": { + "label": "نسخ إلى الحافظة", + "success": "نسخ السجلات إلى الحافظة", + "error": "تعذر نسخ السجلات إلى الحافظة" + }, + "type": { + "label": "النوع", + "timestamp": "الختم الزمني" + }, + "tips": "يتم بث السجلات من الخادم" + }, + "title": "النظام", + "general": { + "hardwareInfo": { + "gpuEncoder": "مشفر ترميز GPU", + "gpuDecoder": "مفكك ترميز GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "مخرجات Vainfo", + "processOutput": "ناتج العملية:", + "processError": "خطأ في العملية:" + }, + "nvidiaSMIOutput": { + "title": "مخرجات Nvidia SMI", + "name": "الاسم: {{name}}", + "driver": "برنامج التشغيل: {{driver}}", + "cudaComputerCapability": "قدرة الحوسبة CUDA: {{cuda_compute}}" + } + }, + "title": "معلومات الاجهزة المادية", + "gpuUsage": "مقدار استخدام GPU", + "gpuMemory": "ذاكرة GPU" + }, + "title": "لمحة عامة", + "detector": { + "title": "أجهزة الكشف", + "inferenceSpeed": "سرعة استنتاج الكاشف", + "temperature": "درجة حرارة الكاشف", + "cpuUsage": "كشف استخدام CPU", + "memoryUsage": "كشف استخدام الذاكرة" + }, + "otherProcesses": { + "title": "عمليات أخرى", + "processCpuUsage": "استخدام وحدة المعالجة المركزية (CPU)", + "processMemoryUsage": "استخدام ذاكرة العملية" + } + }, + "storage": { + "title": "التخزين", + "overview": "نظرة عامة", + "recordings": { + "title": "التسجيلات", + "tips": "تمثل هذه القيمة إجمالي مساحة التخزين المستخدمة للتسجيلات في قاعدة بيانات Frigate. لا يتتبع Frigate استخدام مساحة التخزين لجميع الملفات الموجودة على القرص.", + "earliestRecording": "أقدم تسجيل متاح:" + } + }, + "cameras": { + "overview": "نظرة عامة", + "info": { + "unknown": "غير معروف" + } } } diff --git a/web/public/locales/bg/audio.json b/web/public/locales/bg/audio.json index e59baf850..fcc7a3902 100644 --- a/web/public/locales/bg/audio.json +++ b/web/public/locales/bg/audio.json @@ -2,9 +2,9 @@ "babbling": "Бърборене", "whispering": "Шепнене", "laughter": "Смях", - "crying": "Плача", + "crying": "Плач", "sigh": "Въздишка", - "singing": "Подписвам", + "singing": "Пеене", "choir": "Хор", "yodeling": "Йоделинг", "mantra": "Мантра", @@ -264,5 +264,6 @@ "pant": "Здъхване", "stomach_rumble": "Къркорене на стомах", "heartbeat": "Сърцебиене", - "scream": "Вик" + "scream": "Вик", + "snicker": "Хихикане" } diff --git a/web/public/locales/bg/common.json b/web/public/locales/bg/common.json index 8c5519885..94e85ddd9 100644 --- a/web/public/locales/bg/common.json +++ b/web/public/locales/bg/common.json @@ -56,7 +56,14 @@ "formattedTimestampMonthDayYear": { "12hour": "МММ д, гггг", "24hour": "МММ д, гггг" - } + }, + "ago": "Преди {{timeAgo}}", + "untilForTime": "До {{time}}", + "untilForRestart": "Докато Frigate рестартира.", + "untilRestart": "До рестарт", + "mo": "{{time}}мес", + "m": "{{time}}м", + "s": "{{time}}с" }, "button": { "apply": "Приложи", @@ -106,5 +113,7 @@ }, "label": { "back": "Върни се" - } + }, + "selectItem": "Избери {{item}}", + "readTheDocumentation": "Прочетете документацията" } diff --git a/web/public/locales/bg/components/auth.json b/web/public/locales/bg/components/auth.json index 0967ef424..094cd71a0 100644 --- a/web/public/locales/bg/components/auth.json +++ b/web/public/locales/bg/components/auth.json @@ -1 +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 index e95016ad9..cad1127a0 100644 --- a/web/public/locales/bg/components/camera.json +++ b/web/public/locales/bg/components/camera.json @@ -7,7 +7,7 @@ "label": "Изтрий група за камери", "confirm": { "title": "Потвърди изтриването", - "desc": "Сигурни ли сте, че искате да изтриете група {{name}}?" + "desc": "Сигурни ли сте, че искате да изтриете група {{name}}?" } }, "name": { diff --git a/web/public/locales/bg/components/dialog.json b/web/public/locales/bg/components/dialog.json index d58e72203..6a2d356b5 100644 --- a/web/public/locales/bg/components/dialog.json +++ b/web/public/locales/bg/components/dialog.json @@ -8,5 +8,12 @@ "lastHour_other": "Последните {{count}} часа" }, "select": "Избери" + }, + "restart": { + "title": "Сигурен ли сте, че искате да рестартирате Frigate?", + "button": "Рестартирай", + "restarting": { + "title": "Frigare се рестартира" + } } } diff --git a/web/public/locales/bg/components/icons.json b/web/public/locales/bg/components/icons.json index 0967ef424..a978fa345 100644 --- a/web/public/locales/bg/components/icons.json +++ b/web/public/locales/bg/components/icons.json @@ -1 +1,8 @@ -{} +{ + "iconPicker": { + "selectIcon": "Изберете иконка", + "search": { + "placeholder": "Потърси за икона…" + } + } +} diff --git a/web/public/locales/bg/components/input.json b/web/public/locales/bg/components/input.json index 0967ef424..9bd41d676 100644 --- a/web/public/locales/bg/components/input.json +++ b/web/public/locales/bg/components/input.json @@ -1 +1,10 @@ -{} +{ + "button": { + "downloadVideo": { + "label": "Свали видео", + "toast": { + "success": "Вашето видео за преглеждане почна да се изтегля." + } + } + } +} diff --git a/web/public/locales/bg/objects.json b/web/public/locales/bg/objects.json index bb4ad4968..a12c53c3a 100644 --- a/web/public/locales/bg/objects.json +++ b/web/public/locales/bg/objects.json @@ -18,5 +18,6 @@ "bicycle": "Велосипед", "skateboard": "Скейтборд", "door": "Врата", - "blender": "Блендер" + "blender": "Блендер", + "person": "Човек" } diff --git a/web/public/locales/bg/views/classificationModel.json b/web/public/locales/bg/views/classificationModel.json new file mode 100644 index 000000000..7b8ecb1dd --- /dev/null +++ b/web/public/locales/bg/views/classificationModel.json @@ -0,0 +1,6 @@ +{ + "documentTitle": "Модели за класификация - Frigate", + "description": { + "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." + } +} diff --git a/web/public/locales/bg/views/configEditor.json b/web/public/locales/bg/views/configEditor.json index 0967ef424..955fb99b7 100644 --- a/web/public/locales/bg/views/configEditor.json +++ b/web/public/locales/bg/views/configEditor.json @@ -1 +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 index c355c8bec..affd0cb52 100644 --- a/web/public/locales/bg/views/events.json +++ b/web/public/locales/bg/views/events.json @@ -9,5 +9,10 @@ "aria": "Избери събития", "noFoundForTimePeriod": "Няма намерени събития за този времеви период." }, - "allCameras": "Всички камери" + "allCameras": "Всички камери", + "alerts": "Известия", + "detections": "Засичания", + "motion": { + "label": "Движение" + } } diff --git a/web/public/locales/bg/views/explore.json b/web/public/locales/bg/views/explore.json index ab04d4746..d6c074d4e 100644 --- a/web/public/locales/bg/views/explore.json +++ b/web/public/locales/bg/views/explore.json @@ -8,5 +8,7 @@ } }, "trackedObjectsCount_one": "{{count}} проследен обект ", - "trackedObjectsCount_other": "{{count}} проследени обекта " + "trackedObjectsCount_other": "{{count}} проследени обекта ", + "documentTitle": "Разгледай - Фригейт", + "generativeAI": "Генеративен Изкъствен Интелект" } diff --git a/web/public/locales/bg/views/exports.json b/web/public/locales/bg/views/exports.json index 0967ef424..5454a085d 100644 --- a/web/public/locales/bg/views/exports.json +++ b/web/public/locales/bg/views/exports.json @@ -1 +1,23 @@ -{} +{ + "documentTitle": "Експорт - Frigate", + "search": "Търси", + "noExports": "Няма намерени експорти", + "deleteExport": "Изтрий експорт", + "deleteExport.desc": "Сигурни ли сте, че искате да изтриете {{exportName}}?", + "editExport": { + "title": "Преименувай експорт", + "desc": "Въведете ново име за този експорт.", + "saveExport": "Запази експорт" + }, + "tooltip": { + "shareExport": "Сподели експорт", + "downloadVideo": "Свали видео", + "editName": "Редактирай име", + "deleteExport": "Изтрий експорт" + }, + "toast": { + "error": { + "renameExportFailed": "Неуспешно преименуване на експорт: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/bg/views/faceLibrary.json b/web/public/locales/bg/views/faceLibrary.json index a461ee3df..7d4a82211 100644 --- a/web/public/locales/bg/views/faceLibrary.json +++ b/web/public/locales/bg/views/faceLibrary.json @@ -10,5 +10,10 @@ "deletedName_one": "{{count}} лице бе изтрито успешно.", "deletedName_other": "{{count}} лица бяха изтрити успешно." } + }, + "description": { + "addFace": "Добавете нова колекция във библиотеката за лица при качването на първата ви снимка.", + "placeholder": "Напишете име за тази колекция", + "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." } } diff --git a/web/public/locales/bg/views/live.json b/web/public/locales/bg/views/live.json index c1b6ac1dc..01b3a5c34 100644 --- a/web/public/locales/bg/views/live.json +++ b/web/public/locales/bg/views/live.json @@ -63,5 +63,7 @@ }, "cameraSettings": { "cameraEnabled": "Камерата е включена" - } + }, + "documentTitle": "Наживо - Frigate", + "documentTitle.withCamera": "{{camera}} - На живо - Фригейт" } diff --git a/web/public/locales/bg/views/search.json b/web/public/locales/bg/views/search.json index 8f710c14b..924682386 100644 --- a/web/public/locales/bg/views/search.json +++ b/web/public/locales/bg/views/search.json @@ -1,5 +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 index 830e0ffe8..08395e4db 100644 --- a/web/public/locales/bg/views/settings.json +++ b/web/public/locales/bg/views/settings.json @@ -12,5 +12,9 @@ "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 index 39c14cb20..be1e23db1 100644 --- a/web/public/locales/bg/views/system.json +++ b/web/public/locales/bg/views/system.json @@ -1,5 +1,10 @@ { "stats": { "healthy": "Системата е изправна" + }, + "documentTitle": { + "cameras": "Статистики за Камери - Фригейт", + "storage": "Статистика за паметта - Фригейт", + "general": "Обща Статистика - Frigate" } } diff --git a/web/public/locales/ca/audio.json b/web/public/locales/ca/audio.json index 1af579479..27c44b40e 100644 --- a/web/public/locales/ca/audio.json +++ b/web/public/locales/ca/audio.json @@ -425,5 +425,79 @@ "radio": "Ràdio", "pink_noise": "Soroll rosa", "power_windows": "Finestres elèctriques", - "artillery_fire": "Foc d'artilleria" + "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": "To de grinyol", + "harmonic": "Harmònic", + "sine_wave": "Ona sinus", + "crunch": "Cruixit", + "hum": "Taral·lejar", + "plop": "Chof", + "clickety_clack": "Clic-Clac", + "clicking": "Clicant", + "clatter": "Soroll", + "chird": "Piular", + "liquid": "Líquid", + "splash": "Xof", + "slosh": "Xip-xap", + "boing": "Boing", + "zing": "Fiu", + "rumble": "Bum-bum", + "sizzle": "Xiu-xiu", + "whir": "Brrrm", + "rustle": "Fru-Fru", + "creak": "Clic-clac", + "clang": "Clang", + "squish": "Xaf", + "drip": "Plic-plic", + "pour": "Glug-glug", + "trickle": "Xiulet", + "gush": "Xuuuix", + "fill": "Glug-glug", + "ding": "Ding", + "ping": "Ping", + "beep": "Bip", + "squeal": "Xiscle", + "crumpling": "Arrugant-se", + "rub": "Fregar", + "scrape": "Raspar", + "scratch": "Rasca", + "whip": "Fuet", + "bouncing": "Rebotant", + "breaking": "Trencant", + "smash": "Aixafar", + "whack": "Cop", + "slap": "Bufetada", + "bang": "Bang", + "basketball_bounce": "Rebot de bàsquet", + "chorus_effect": "Efecte de cor", + "effects_unit": "Unitat d'Efectes", + "electronic_tuner": "Afinador electrònic", + "thunk": "Bruix", + "thump": "Cop fort", + "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 index c981fd716..61faabea0 100644 --- a/web/public/locales/ca/common.json +++ b/web/public/locales/ca/common.json @@ -40,7 +40,15 @@ "sk": "Slovenčina (Eslovac)", "ru": "Русский (Rus)", "th": "ไทย (Tailandès)", - "ca": "Català (Catalan)" + "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ú)" }, "system": "Sistema", "systemMetrics": "Mètriques del sistema", @@ -96,7 +104,8 @@ "anonymous": "Anònim", "logout": "Tanca la sessió", "current": "Usuari actual: {{user}}" - } + }, + "classification": "Classificació" }, "pagination": { "previous": { @@ -189,7 +198,10 @@ "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" }, "unit": { "speed": { @@ -199,10 +211,24 @@ "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" + "back": "Torna enrere", + "hide": "Oculta {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Cap", + "all": "Tots", + "other": "Altres" }, "button": { "apply": "Aplicar", @@ -239,7 +265,8 @@ "off": "APAGAT", "unselect": "Desseleccionar", "enable": "Habilitar", - "enabled": "Habilitat" + "enabled": "Habilitat", + "continue": "Continua" }, "toast": { "copyUrlToClipboard": "URL copiada al porta-retalls.", @@ -261,5 +288,18 @@ "title": "404", "desc": "Pàgina no trobada" }, - "selectItem": "Selecciona {{item}}" + "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" + } } diff --git a/web/public/locales/ca/components/auth.json b/web/public/locales/ca/components/auth.json index 5d4b413a6..1ca91ee7a 100644 --- a/web/public/locales/ca/components/auth.json +++ b/web/public/locales/ca/components/auth.json @@ -1,6 +1,6 @@ { "form": { - "user": "Nom d'usuari", + "user": "Usuari", "password": "Contrasenya", "login": "Iniciar sessió", "errors": { @@ -10,6 +10,7 @@ "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 index b93a84a5f..bfa8ea161 100644 --- a/web/public/locales/ca/components/camera.json +++ b/web/public/locales/ca/components/camera.json @@ -63,11 +63,12 @@ "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" + "label": "Grups de Càmeres" }, "debug": { "options": { diff --git a/web/public/locales/ca/components/dialog.json b/web/public/locales/ca/components/dialog.json index b2759e896..79e4bd864 100644 --- a/web/public/locales/ca/components/dialog.json +++ b/web/public/locales/ca/components/dialog.json @@ -53,12 +53,13 @@ "export": "Exportar", "selectOrExport": "Seleccionar o exportar", "toast": { - "success": "Exportació inciada amb èxit. Pots veure l'arxiu a la carpeta /exports.", + "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" }, "fromTimeline": { "saveExport": "Guardar exportació", @@ -98,7 +99,8 @@ "button": { "deleteNow": "Suprimir ara", "export": "Exportar", - "markAsReviewed": "Marcar com a revisat" + "markAsReviewed": "Marcar com a revisat", + "markAsUnreviewed": "Marcar com no revisat" }, "confirmDelete": { "title": "Confirmar la supressió", @@ -110,5 +112,13 @@ "error": "No s'ha pogut suprimir: {{error}}" } } + }, + "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 index aa02310f7..5a0a7b083 100644 --- a/web/public/locales/ca/components/filter.json +++ b/web/public/locales/ca/components/filter.json @@ -108,7 +108,9 @@ "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." + "selectPlatesFromList": "Seleccioni una o més matrícules de la llista.", + "selectAll": "Seleccionar tots", + "clearAll": "Netejar tot" }, "cameras": { "label": "Filtre de càmeres", @@ -122,5 +124,17 @@ }, "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/views/classificationModel.json b/web/public/locales/ca/views/classificationModel.json new file mode 100644 index 000000000..7a9a7571d --- /dev/null +++ b/web/public/locales/ca/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "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": "Classe suprimida", + "deletedImage": "Imatges suprimides", + "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}}" + }, + "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 fragata 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}}" + } + }, + "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 d'estat", + "description": "Es recomana seleccionar exemples per a tots els estats per obtenir els millors resultats. Podeu continuar sense seleccionar tots els estats, però el model no serà entrenat fins que tots els estats tinguin imatges. Després de continuar, utilitzeu la vista Classificacions recents per classificar imatges per als estats que falten, i després entrenar el model." + } + } + }, + "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" +} diff --git a/web/public/locales/ca/views/configEditor.json b/web/public/locales/ca/views/configEditor.json index 8d47ea04c..bd3149a3f 100644 --- a/web/public/locales/ca/views/configEditor.json +++ b/web/public/locales/ca/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Error al desar la configuració" } }, - "confirm": "Sortir sense desar?" + "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 index 1a219b9c1..5f3c5ea95 100644 --- a/web/public/locales/ca/views/events.json +++ b/web/public/locales/ca/views/events.json @@ -10,7 +10,11 @@ "empty": { "alert": "Hi ha cap alerta per revisar", "detection": "Hi ha cap detecció per revisar", - "motion": "No s'haan trobat dades de moviment" + "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": "Línia de temps", "timeline.aria": "Seleccionar línia de temps", @@ -34,5 +38,30 @@ }, "camera": "Càmera", "selected_one": "{{count}} seleccionats", - "selected_other": "{{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" } diff --git a/web/public/locales/ca/views/explore.json b/web/public/locales/ca/views/explore.json index 07f787ed3..2c94e50f5 100644 --- a/web/public/locales/ca/views/explore.json +++ b/web/public/locales/ca/views/explore.json @@ -84,7 +84,9 @@ "details": "detalls", "snapshot": "instantània", "video": "vídeo", - "object_lifecycle": "cicle de vida de l'objecte" + "object_lifecycle": "cicle de vida de l'objecte", + "thumbnail": "miniatura", + "tracking_details": "detalls del seguiment" }, "details": { "timestamp": "Marca temporal", @@ -97,12 +99,16 @@ "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." + "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}}" + "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", @@ -155,6 +161,17 @@ "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" } }, "searchResult": { @@ -164,7 +181,9 @@ "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": { @@ -193,17 +212,94 @@ }, "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" } }, "noTrackedObjects": "No s'han trobat objectes rastrejats", "dialog": { "confirmDelete": { "title": "Confirmar la supressió", - "desc": "Eliminant aquest objecte seguit borrarà l'snapshot, qualsevol embedding gravat, i qualsevol objecte associat. Les imatges gravades d'aquest objecte seguit en l'historial NO seràn eliminades.

    Estas segur que vols continuar?" + "desc": "Eliminant aquest objecte seguit borrarà l'snapshot, qualsevol embedding gravat, i qualsevol detall de seguiment. Les imatges gravades d'aquest objecte seguit en l'historial NO seràn eliminades.

    Estas segur que vols continuar?" } }, "fetchingTrackedObjectsFailed": "Error al obtenir objectes rastrejats: {{errorMessage}}", "trackedObjectsCount_one": "{{count}} objecte rastrejat ", "trackedObjectsCount_many": "{{count}} objectes rastrejats ", - "trackedObjectsCount_other": "{{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}} esquerra", + "heard": "{{label}} sentit", + "external": "{{label}} detectat", + "header": { + "zones": "Zones", + "ratio": "Ràtio", + "area": "Àrea", + "score": "Puntuació" + } + }, + "annotationSettings": { + "title": "Configuració d'anotacions", + "showAllZones": { + "title": "Mostra totes les Zones", + "desc": "Mostra sempre les zones amb marcs 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 index dfe5de963..dec2726ff 100644 --- a/web/public/locales/ca/views/exports.json +++ b/web/public/locales/ca/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Comparteix l'exportació", + "downloadVideo": "Baixa el vídeo", + "editName": "Edita el nom", + "deleteExport": "Suprimeix l'exportació" } } diff --git a/web/public/locales/ca/views/faceLibrary.json b/web/public/locales/ca/views/faceLibrary.json index 356691315..d2a5fcf34 100644 --- a/web/public/locales/ca/views/faceLibrary.json +++ b/web/public/locales/ca/views/faceLibrary.json @@ -12,13 +12,14 @@ "collections": "Col·leccions", "train": { "empty": "No hi ha intents recents de reconeixement de rostres", - "title": "Entrenar", - "aria": "Seleccionar entrenament" + "title": "Reconeixements recents", + "aria": "Selecciona els reconeixements recents", + "titleShort": "Recent" }, "description": { - "addFace": "Guia per a agregar una nova colecció a la biblioteca de rostres.", + "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 guionets." + "invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions." }, "documentTitle": "Biblioteca de rostres - Frigate", "uploadFaceImage": { @@ -54,7 +55,7 @@ "selectImage": "Siusplau, selecciona un fixer d'imatge." }, "maxSize": "Mida màxima: {{size}}MB", - "dropInstructions": "Arrastra una imatge aquí, o fes clic per a selccionar-ne una" + "dropInstructions": "Arrossegueu i deixeu anar o enganxeu una imatge aquí, o feu clic per seleccionar" }, "button": { "uploadImage": "Pujar imatge", @@ -67,7 +68,7 @@ "toast": { "success": { "trainedFace": "Rostre entrenat amb èxit.", - "updatedFaceScore": "Puntació de rostre actualitzada 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.", diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json index dd091b7de..d9245fe7c 100644 --- a/web/public/locales/ca/views/live.json +++ b/web/public/locales/ca/views/live.json @@ -32,7 +32,15 @@ "label": "Fer clic a la imatge per centrar la càmera PTZ" } }, - "presets": "Predefinits de 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": "Directe - Frigate", "documentTitle.withCamera": "{{camera}} - Directe - Frigate", @@ -78,8 +86,8 @@ "disable": "Amaga estadístiques de la transmissió" }, "manualRecording": { - "title": "Gravació sota demanda", - "tips": "Iniciar un event manual basat en els paràmetres de retenció de gravació per aquesta càmera.", + "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." @@ -122,6 +130,9 @@ "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ó", @@ -135,7 +146,8 @@ "snapshots": "Instantànies", "autotracking": "Seguiment automàtic", "objectDetection": "Detecció d'objectes", - "audioDetection": "Detecció d'àudio" + "audioDetection": "Detecció d'àudio", + "transcription": "Transcripció d'audio" }, "history": { "label": "Mostrar gravacions històriques" @@ -154,5 +166,24 @@ "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." + } } } diff --git a/web/public/locales/ca/views/search.json b/web/public/locales/ca/views/search.json index 3f5940348..71f333180 100644 --- a/web/public/locales/ca/views/search.json +++ b/web/public/locales/ca/views/search.json @@ -15,7 +15,8 @@ "max_speed": "Velocitat màxima", "recognized_license_plate": "Matrícula reconeguda", "has_clip": "Té Clip", - "has_snapshot": "Té instantània" + "has_snapshot": "Té instantània", + "attributes": "Atributs" }, "searchType": { "thumbnail": "Miniatura", @@ -55,12 +56,12 @@ "searchFor": "Buscar {{inputValue}}", "button": { "clear": "Netejar cerca", - "save": "Desar la cerca", - "delete": "Suprimeix la recerca desada", - "filterInformation": "Informació de filtre", + "save": "Desa la cerca", + "delete": "Elimina la recerca desada", + "filterInformation": "Informació del filtre", "filterActive": "Filtres actius" }, - "trackedObjectId": "ID d'objecte rastrejat", + "trackedObjectId": "ID de l'objecte rastrejat", "placeholder": { "search": "Cercar…" }, diff --git a/web/public/locales/ca/views/settings.json b/web/public/locales/ca/views/settings.json index a94e86bd1..88b75bca6 100644 --- a/web/public/locales/ca/views/settings.json +++ b/web/public/locales/ca/views/settings.json @@ -7,9 +7,11 @@ "authentication": "Configuració d'autenticació - Frigate", "camera": "Paràmetres de càmera - Frigate", "masksAndZones": "Editor de màscares i zones - Frigate", - "general": "Paràmetres Generals - Frigate", + "general": "Configuració de la interfície d'usuari - Fragata", "frigatePlus": "Paràmetres de Frigate+ - Frigate", - "notifications": "Paràmetres de notificació - Frigate" + "notifications": "Paràmetres de notificació - Frigate", + "cameraManagement": "Gestionar càmeres - Frigate", + "cameraReview": "Configuració Revisió de Càmeres - Frigate" }, "menu": { "ui": "Interfície d'usuari", @@ -20,7 +22,11 @@ "notifications": "Notificacions", "debug": "Depuració", "frigateplus": "Frigate+", - "enrichments": "Enriquiments" + "enrichments": "Enriquiments", + "triggers": "Disparadors", + "cameraManagement": "Gestió", + "cameraReview": "Revisió", + "roles": "Rols" }, "dialog": { "unsavedChanges": { @@ -33,7 +39,7 @@ "noCamera": "Cap càmera" }, "general": { - "title": "Paràmetres generals", + "title": "Paràmetres de la interfície d'usuari", "liveDashboard": { "title": "Panell en directe", "automaticLiveView": { @@ -43,6 +49,14 @@ "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": { @@ -108,7 +122,8 @@ "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." + "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": { @@ -157,7 +172,7 @@ "name": { "inputPlaceHolder": "Introduïu un nom…", "title": "Nom", - "tips": "El nom ha de tenir almenys 2 caràcters i no pot coincidir amb el nom d'una càmera ni amb el d'una altra zona." + "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": { @@ -184,7 +199,7 @@ }, "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", "toast": { - "success": "La zona {{zoneName}} ha estat desada. Reinicia Frigate per a aplicar els canvis." + "success": "S'ha desat la zona ({{zoneName}})." } }, "filter": { @@ -214,8 +229,8 @@ "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", "toast": { "success": { - "title": "{{polygonName}} s'ha desat. Reinicia Frigate per a aplicar els canvis.", - "noName": "La màscara de moviment ha estat desada. Reinicia Frigate per aplicar els canvis." + "title": "{{polygonName}} s'ha desat.", + "noName": "La màscara de moviment ha estat desada." } } }, @@ -239,8 +254,8 @@ "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", "toast": { "success": { - "title": "{{polygonName}} s'ha desat. Reinicia Frigate per a aplicar els canvis.", - "noName": "La màscara d'objectes ha estat desada. Reincia Frigate per a aplicar els canvis." + "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ó." @@ -344,6 +359,43 @@ "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": { @@ -414,12 +466,25 @@ "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." + "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": "Nom d'usuari", - "password": "Contrasenya", + "username": "Usuari", + "password": "Restableix la contrasenya", "deleteUser": "Suprimir usuari", "noUsers": "No s'han trobat usuaris.", "changeRole": "Canviar la funció d’usuari", @@ -462,7 +527,16 @@ "notMatch": "Les contrasenyes no coincideixen", "match": "Les contrasenyes coincideixen", "placeholder": "Introdueix la contrasenya", - "title": "Contrasenya" + "title": "Contrasenya", + "show": "Mostra contrasenya", + "hide": "Amaga contrasenya", + "requirements": { + "title": "Requisits contrasenya:", + "length": "Com a mínim 8 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", @@ -472,14 +546,23 @@ } }, "usernameIsRequired": "El nom d'usuari és obligatori", - "passwordIsRequired": "La contrasenya és obligatoria" + "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." + "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", @@ -492,7 +575,8 @@ "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ó." + "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}}", @@ -511,7 +595,7 @@ "title": "Gestió d'usuaris", "desc": "Gestioneu els comptes d'usuari d'aquesta instància de Frigate." }, - "updatePassword": "Actualitzar contrasenya" + "updatePassword": "Restableix la contrasenya" }, "frigatePlus": { "snapshotConfig": { @@ -612,11 +696,553 @@ "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": "Parmàmetres complementaris", + "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://\". 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": "SS'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." + } + } + } + }, + "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": "Habilita / Inhabilita les càmeres", + "desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.
    Nota: això no desactiva les retransmissions de go2rtc." + }, + "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" + } + } + }, + "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. 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 revisió de la IA generativa per a aquesta càmera. 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 la fragata 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" } } diff --git a/web/public/locales/ca/views/system.json b/web/public/locales/ca/views/system.json index d4d63a31d..312f3c299 100644 --- a/web/public/locales/ca/views/system.json +++ b/web/public/locales/ca/views/system.json @@ -41,7 +41,8 @@ "title": "Detectors", "inferenceSpeed": "Velocitat d'inferència del detector", "cpuUsage": "Ús de CPU del detector", - "temperature": "Temperatura 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": { @@ -75,12 +76,24 @@ } }, "npuUsage": "Ús de NPU", - "npuMemory": "Memòria 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 fragata. Podeu reiniciar l'amfitrió per a corregir temporalment el problema i confirmar que la GPU funciona correctament. Això no afecta el rendiment." + } }, "otherProcesses": { "title": "Altres processos", "processMemoryUsage": "Ús de memòria de procés", - "processCpuUsage": "Ús de la CPU del procés" + "processCpuUsage": "Ús de la CPU del procés", + "series": { + "recording": "gravant", + "review_segment": "segment de revisió", + "embeddings": "incrustacions", + "audio_detector": "detector d'àudio", + "go2rtc": "go2rtc" + } } }, "storage": { @@ -102,7 +115,11 @@ }, "percentageOfTotalUsed": "Percentatge del total" }, - "overview": "Visió general" + "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." + } }, "cameras": { "framesAndDetections": "Fotogrames / Deteccions", @@ -158,7 +175,8 @@ "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)" + "detectIsSlow": "{{detect}} és lent ({{speed}} ms)", + "shmTooLow": "/dev/shm directori ({{total}} MB) hauria de ser incrementat com a mínim {{min}} MB." }, "enrichments": { "title": "Enriquiments", @@ -173,8 +191,18 @@ "plate_recognition_speed": "Velocitat de reconeixement de matrícules", "text_embedding_speed": "Velocitat d'incrustació de text", "yolov9_plate_detection": "Detecció de matrícules YOLOv9", - "yolov9_plate_detection_speed": "Velocitat de 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" + "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 index 4308f7487..8876626ac 100644 --- a/web/public/locales/cs/audio.json +++ b/web/public/locales/cs/audio.json @@ -53,7 +53,7 @@ "moo": "Bučení", "cowbell": "Kravský zvonec", "pig": "Prase", - "oink": "Chrochtání", + "oink": "Chrochtanie", "fowl": "Drůbež", "chicken": "Slepice", "cluck": "Kvokání", diff --git a/web/public/locales/cs/common.json b/web/public/locales/cs/common.json index 08cff4992..856c88a63 100644 --- a/web/public/locales/cs/common.json +++ b/web/public/locales/cs/common.json @@ -130,7 +130,7 @@ "meters": "metry" } }, - "selectItem": "Vyberte {{item}}", + "selectItem": "Vybrat {{item}}", "menu": { "documentation": { "label": "Dokumentace Frigate", @@ -185,7 +185,15 @@ "hu": "Magyar (Maďarština)", "pl": "Polski (Polština)", "th": "ไทย (Thaiština)", - "ca": "Català (Katalánština)" + "ca": "Català (Katalánština)", + "sl": "Slovinština (Slovinsko)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "sr": "Српски (Serbian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)" }, "theme": { "highcontrast": "Vysoký kontrast", @@ -261,5 +269,6 @@ "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" } diff --git a/web/public/locales/cs/components/auth.json b/web/public/locales/cs/components/auth.json index a3dd01b32..00b0160cb 100644 --- a/web/public/locales/cs/components/auth.json +++ b/web/public/locales/cs/components/auth.json @@ -10,6 +10,7 @@ "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 index 2c3f0d6c7..ef56aa729 100644 --- a/web/public/locales/cs/components/camera.json +++ b/web/public/locales/cs/components/camera.json @@ -41,7 +41,8 @@ "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": { diff --git a/web/public/locales/cs/components/dialog.json b/web/public/locales/cs/components/dialog.json index 53318710a..8b982edcd 100644 --- a/web/public/locales/cs/components/dialog.json +++ b/web/public/locales/cs/components/dialog.json @@ -110,5 +110,12 @@ "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" } } diff --git a/web/public/locales/cs/components/filter.json b/web/public/locales/cs/components/filter.json index d057b6e7d..55ff667c1 100644 --- a/web/public/locales/cs/components/filter.json +++ b/web/public/locales/cs/components/filter.json @@ -92,7 +92,9 @@ "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." + "noLicensePlatesFound": "Žádné SPZ nebyly nalezeny.", + "selectAll": "Označit vše", + "clearAll": "Vymazat vše" }, "zones": { "all": { @@ -122,5 +124,13 @@ }, "review": { "showReviewed": "Zobrazit zkontrolované" + }, + "classes": { + "label": "Třídy", + "all": { + "title": "Všechny třídy" + }, + "count_one": "Třída {{count}}", + "count_other": "Třídy {{count}}" } } diff --git a/web/public/locales/cs/objects.json b/web/public/locales/cs/objects.json index f25710235..ca2092069 100644 --- a/web/public/locales/cs/objects.json +++ b/web/public/locales/cs/objects.json @@ -111,7 +111,7 @@ "fedex": "FedEx", "dhl": "DHL", "an_post": "An Post", - "purolator": "Purolator", + "purolator": "Čistič", "postnl": "PostNL", "nzpost": "NZPost", "postnord": "PostNord", diff --git a/web/public/locales/cs/views/classificationModel.json b/web/public/locales/cs/views/classificationModel.json new file mode 100644 index 000000000..f3713a255 --- /dev/null +++ b/web/public/locales/cs/views/classificationModel.json @@ -0,0 +1,47 @@ +{ + "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": "Smazat obrázky", + "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": "Smazat třídu", + "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}}" + } + }, + "train": { + "titleShort": "Nedávný" + } +} diff --git a/web/public/locales/cs/views/configEditor.json b/web/public/locales/cs/views/configEditor.json index 55fbafb2c..19982f20c 100644 --- a/web/public/locales/cs/views/configEditor.json +++ b/web/public/locales/cs/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Chyba ukládání konfigurace" } }, - "confirm": "Opustit bez uložení?" + "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 index 17cade7e0..d05bd7cdc 100644 --- a/web/public/locales/cs/views/events.json +++ b/web/public/locales/cs/views/events.json @@ -34,5 +34,16 @@ }, "detected": "Detekováno", "selected_one": "{{count}} vybráno", - "selected_other": "{{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 detailní zobrazení", + "trackedObject_other": "{{count}} objektů", + "trackedObject_one": "{{count}} objektů" + } } diff --git a/web/public/locales/cs/views/explore.json b/web/public/locales/cs/views/explore.json index 1dba5c605..8acdd2386 100644 --- a/web/public/locales/cs/views/explore.json +++ b/web/public/locales/cs/views/explore.json @@ -23,12 +23,14 @@ "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." + "updatedLPR": "Úspěšně aktualizovaná SPZ.", + "audioTranscription": "Požádání o přepis zvuku bylo úspěšné." }, "error": { "regenerate": "Chyba volání {{provider}} pro nový popis: {{errorMessage}}", "updatedSublabelFailed": "Chyba obnovení podružného štítku: {{errorMessage}}", - "updatedLPRFailed": "Chyba obnovení SPZ: {{errorMessage}}" + "updatedLPRFailed": "Chyba obnovení SPZ: {{errorMessage}}", + "audioTranscription": "Požádání o přepis zvuku bylo neúspěšné: {{errorMessage}}" } } }, @@ -70,7 +72,10 @@ "label": "Nejvyšší skóre" }, "label": "Štítek", - "recognizedLicensePlate": "Rozpoznaná SPZ" + "recognizedLicensePlate": "Rozpoznaná SPZ", + "score": { + "label": "Skóre" + } }, "exploreIsUnavailable": { "title": "Prozkoumat je nedostupné", @@ -188,6 +193,14 @@ "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" } }, "dialog": { @@ -205,5 +218,11 @@ }, "noTrackedObjects": "Žádné sledované objekty nebyly nalezeny", "fetchingTrackedObjectsFailed": "Chyba při načítání sledovaných objektů: {{errorMessage}}", - "exploreMore": "Prozkoumat více {{label}} objektů" + "exploreMore": "Prozkoumat více {{label}} objektů", + "aiAnalysis": { + "title": "Analýza AI" + }, + "concerns": { + "label": "Obavy" + } } diff --git a/web/public/locales/cs/views/exports.json b/web/public/locales/cs/views/exports.json index d27bf05e9..5fb25d638 100644 --- a/web/public/locales/cs/views/exports.json +++ b/web/public/locales/cs/views/exports.json @@ -13,5 +13,11 @@ "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 index 8db564c37..cf4b1faea 100644 --- a/web/public/locales/cs/views/faceLibrary.json +++ b/web/public/locales/cs/views/faceLibrary.json @@ -36,12 +36,13 @@ "desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje." }, "train": { - "title": "Trénovat", + "title": "Nedávná rozpoznání", "empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje", - "aria": "Vybrat trénink" + "aria": "Vybrat poslední rozpoznávání", + "titleShort": "Nedávný" }, "description": { - "addFace": "Prúvodce přidání nové kolekce do Knižnice obličejů.", + "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ý název. Názvy mohou obsahovat pouze písmena, čísla, mezery, apostrofy, podtržítka a pomlčky." }, @@ -76,7 +77,7 @@ "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.", + "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": { diff --git a/web/public/locales/cs/views/live.json b/web/public/locales/cs/views/live.json index 1e6004a05..f8e77f659 100644 --- a/web/public/locales/cs/views/live.json +++ b/web/public/locales/cs/views/live.json @@ -43,7 +43,15 @@ "label": "Klikněte do snímku pro vycentrování PTZ kamery" } }, - "presets": "Předvolby PTZ kamery" + "presets": "Předvolby PTZ kamery", + "focus": { + "in": { + "label": "Zaostření PTZ kamery" + }, + "out": { + "label": "Rozostření PTZ kamery" + } + } }, "camera": { "enable": "Povolit kameru", @@ -103,7 +111,7 @@ "forTime": "Pozastavení na: " }, "stream": { - "title": "Stream", + "title": "Proud", "audio": { "tips": { "title": "Zvuk musí být kamerou vysílán a nakonfigurován v go2rtc pro tento stream.", @@ -134,7 +142,8 @@ "snapshots": "Snímky", "audioDetection": "Detekce Zvuku", "autotracking": "Automatické sledování", - "recording": "Nahrávání" + "recording": "Nahrávání", + "transcription": "Zvukový přepis" }, "history": { "label": "Zobrazit historické záznamy" @@ -154,5 +163,9 @@ "label": "Upravit Skupinu Kamer" } }, - "notifications": "Notifikace" + "notifications": "Notifikace", + "transcription": { + "enable": "Povolit živý přepis zvuku", + "disable": "Zakázat živý přepis zvuku" + } } diff --git a/web/public/locales/cs/views/search.json b/web/public/locales/cs/views/search.json index e828a6716..2d699792c 100644 --- a/web/public/locales/cs/views/search.json +++ b/web/public/locales/cs/views/search.json @@ -26,7 +26,8 @@ "min_score": "Minimální Skóre", "recognized_license_plate": "Rozpoznaná SPZ", "has_clip": "Má Klip", - "has_snapshot": "Má Snímek" + "has_snapshot": "Má Snímek", + "attributes": "Atributy" }, "tips": { "desc": { diff --git a/web/public/locales/cs/views/settings.json b/web/public/locales/cs/views/settings.json index 065770762..3875b8269 100644 --- a/web/public/locales/cs/views/settings.json +++ b/web/public/locales/cs/views/settings.json @@ -6,11 +6,13 @@ "classification": "Nastavení klasifikace - Frigate", "notifications": "Nastavení notifikací - Frigate", "masksAndZones": "Editor masky a zón - Frigate", - "motionTuner": "Ladič detekce pohybu - Frigate", + "motionTuner": "Ladění detekce pohybu - Frigate", "object": "Ladění - Frigate", - "general": "Obecné nastavení - Frigate", + "general": "Nastavení rozhraní - Frigate", "frigatePlus": "Frigate+ nastavení - Frigate", - "enrichments": "Nastavení obohacení - Frigate" + "enrichments": "Nastavení obohacení - Frigate", + "cameraManagement": "Správa kamer - Frigate", + "cameraReview": "Nastavení kontroly kamery - Frigate" }, "frigatePlus": { "toast": { @@ -298,12 +300,16 @@ "classification": "Klasifikace", "cameras": "Nastavení kamery", "masksAndZones": "Masky / Zóny", - "motionTuner": "Ladič detekce pohybu", + "motionTuner": "Ladění detekce pohybu", "debug": "Ladění", "users": "Uživatelé", "notifications": "Notifikace", - "frigateplus": "Frigate +", - "enrichments": "Obohacení" + "frigateplus": "Frigate+", + "enrichments": "Obohacení", + "triggers": "Spouštěče", + "cameraManagement": "Správa", + "cameraReview": "Kontrola", + "roles": "Role" }, "dialog": { "unsavedChanges": { @@ -318,7 +324,7 @@ "general": { "title": "Hlavní nastavení", "liveDashboard": { - "title": "Živý Dashboard", + "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" @@ -339,9 +345,9 @@ "clearAll": "Vymazat všechna nastavení streamování" }, "recordingsViewer": { - "title": "Prohlížeč Nahrávek", + "title": "Prohlížeč nahrávek", "defaultPlaybackRate": { - "label": "Výchozí Rychlost Přehrávání", + "label": "Výchozí rychlost přehrávání", "desc": "Výchozí rychlost přehrávání pro nahrávky." } }, @@ -375,9 +381,9 @@ "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": "Ladit", + "title": "Ladění", "detectorDesc": "Frigate používá vaše detektory {{detectors}} k detekci objektů ve streamu vašich kamer.", - "objectList": "Seznam Objektů", + "objectList": "Seznam objektů", "boundingBoxes": { "title": "Ohraničující rámečky", "desc": "Zobrazit ohraničující rámečky okolo sledovaných objektů", @@ -394,7 +400,7 @@ "title": "Masky detekce pohybu", "desc": "Zobrazit polygony masek detekce pohybu" }, - "debugging": "Ledění", + "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", @@ -403,13 +409,26 @@ }, "noObjects": "Žádné objekty", "objectShapeFilterDrawing": { - "title": "Kreslení Filtru Tvaru Objektu", + "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": { @@ -444,7 +463,44 @@ }, "limitDetections": "Omezit detekce pro specifické zóny" }, - "title": "Nastavení Kamery" + "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": { @@ -469,7 +525,7 @@ "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í", + "deviceSpecific": "Nastavení specifická pro zařízení", "unregisterDevice": "Odregistrovat Toto Zařízení", "sendTestNotification": "Poslat testovací notifikaci", "unsavedRegistrations": "Neuložené přihlášky k Notifikacím", @@ -553,7 +609,8 @@ "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." + "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}}", @@ -593,13 +650,13 @@ }, "management": { "desc": "Spravujte uživatelské účty této instance Frigate.", - "title": "Správa Uživatelů" + "title": "Správa uživatelů" }, "addUser": "Přidat uživatele", "title": "Uživatelé" }, "motionDetectionTuner": { - "unsavedChanges": "Neuložené změny Ladiče Detekce Pohybu {{camera}}", + "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" @@ -607,9 +664,9 @@ "toast": { "success": "Nastavení detekce pohybu bylo uloženo." }, - "title": "Ladič Detekce Pohybu", + "title": "Ladění detekce pohybu", "desc": { - "documentation": "Přečtěte si příručku Ladiče Detekce Pohybu", + "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": { @@ -624,7 +681,7 @@ "enrichments": { "title": "Nastavení obohacení", "faceRecognition": { - "title": "Rozpoznání Obličeje", + "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": { @@ -651,11 +708,11 @@ "alreadyInProgress": "Přeindexování je již spuštěno.", "error": "Chyba spuštění přeindexování: {{errorMessage}}" }, - "title": "Sémantické Vyhledávání", + "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", + "label": "Velikost modelu", "desc": "Velikost modelu použitého pro vkládání sémantického vyhledávání.", "small": { "title": "malý", @@ -669,7 +726,7 @@ }, "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ů" + "title": "Klasifikace ptáků" }, "unsavedChanges": "Neuložené změny nastavení Obohacení", "licensePlateRecognition": { @@ -682,5 +739,162 @@ "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": "Správa spouštěčů", + "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": "Zadejte název spouštěče", + "error": { + "minLength": "Název musí mít alespoň 2 znaky.", + "invalidCharacters": "Jméno 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": "Vybrat obrázek", + "textPlaceholder": "Zadat textový obsah", + "imageDesc": "Vybrat obrázek, který spustí tuto akci, když bude detekován podobný obrázek.", + "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. Zvolte dodatečnou akci, která se má provést, když se tento spouštěč aktivuje.", + "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ých k této roli bylo aktualizováno na „Divák“, který má přístup ke všem kamerám.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "" + }, + "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." + } } } diff --git a/web/public/locales/cs/views/system.json b/web/public/locales/cs/views/system.json index fca20986f..f920a2159 100644 --- a/web/public/locales/cs/views/system.json +++ b/web/public/locales/cs/views/system.json @@ -52,7 +52,8 @@ "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}}%)" + "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": { @@ -77,7 +78,8 @@ "title": "Detektory", "inferenceSpeed": "Detekční rychlost", "memoryUsage": "Detektor využití paměti", - "cpuUsage": "Detektor využití CPU" + "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", @@ -138,7 +140,11 @@ "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" + "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": { diff --git a/web/public/locales/da/audio.json b/web/public/locales/da/audio.json index 0967ef424..81481336e 100644 --- a/web/public/locales/da/audio.json +++ b/web/public/locales/da/audio.json @@ -1 +1,88 @@ -{} +{ + "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ækkepibe", + "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": "Bore", + "explosion": "Eksplosion", + "fireworks": "Nytårskrudt", + "babbling": "Pludren", + "yell": "Råb", + "whoop": "Jubel", + "snicker": "Smålatter" +} diff --git a/web/public/locales/da/common.json b/web/public/locales/da/common.json index b0bbd3d5f..dbf6ff2d3 100644 --- a/web/public/locales/da/common.json +++ b/web/public/locales/da/common.json @@ -1,6 +1,6 @@ { "time": { - "untilForTime": "Indtil{{time}}", + "untilForTime": "Indtil {{time}}", "untilForRestart": "Indtil Frigate genstarter.", "untilRestart": "Indtil genstart", "ago": "{{timeAgo}} siden", @@ -254,5 +254,6 @@ "title": "404", "desc": "Side ikke fundet" }, - "selectItem": "Vælg {{item}}" + "selectItem": "Vælg {{item}}", + "readTheDocumentation": "Læs dokumentationen" } diff --git a/web/public/locales/da/components/auth.json b/web/public/locales/da/components/auth.json index 0967ef424..ee1018299 100644 --- a/web/public/locales/da/components/auth.json +++ b/web/public/locales/da/components/auth.json @@ -1 +1,15 @@ -{} +{ + "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." + }, + "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 index 0967ef424..769c6cc2a 100644 --- a/web/public/locales/da/components/camera.json +++ b/web/public/locales/da/components/camera.json @@ -1 +1,21 @@ -{} +{ + "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." + } + } + } +} diff --git a/web/public/locales/da/components/dialog.json b/web/public/locales/da/components/dialog.json index 0967ef424..4d4a85174 100644 --- a/web/public/locales/da/components/dialog.json +++ b/web/public/locales/da/components/dialog.json @@ -1 +1,25 @@ -{} +{ + "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." + } + }, + "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}}?" + } + } + } + } +} diff --git a/web/public/locales/da/components/filter.json b/web/public/locales/da/components/filter.json index 0967ef424..3d16c1eb1 100644 --- a/web/public/locales/da/components/filter.json +++ b/web/public/locales/da/components/filter.json @@ -1 +1,50 @@ -{} +{ + "filter": "Filter", + "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 index 0967ef424..44d71dbe3 100644 --- a/web/public/locales/da/components/icons.json +++ b/web/public/locales/da/components/icons.json @@ -1 +1,8 @@ -{} +{ + "iconPicker": { + "selectIcon": "Vælg et ikon", + "search": { + "placeholder": "Søg efter ikoner…" + } + } +} diff --git a/web/public/locales/da/components/input.json b/web/public/locales/da/components/input.json index 0967ef424..0a8c89716 100644 --- a/web/public/locales/da/components/input.json +++ b/web/public/locales/da/components/input.json @@ -1 +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 index 0967ef424..099369052 100644 --- a/web/public/locales/da/components/player.json +++ b/web/public/locales/da/components/player.json @@ -1 +1,15 @@ -{} +{ + "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 denne frame 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 frames på {{cameraName}}-detect-streamen, tjek fejlloggene." + } +} diff --git a/web/public/locales/da/objects.json b/web/public/locales/da/objects.json index 0967ef424..e055dcf4a 100644 --- a/web/public/locales/da/objects.json +++ b/web/public/locales/da/objects.json @@ -1 +1,18 @@ -{} +{ + "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" +} diff --git a/web/public/locales/da/views/classificationModel.json b/web/public/locales/da/views/classificationModel.json new file mode 100644 index 000000000..a3aa81f28 --- /dev/null +++ b/web/public/locales/da/views/classificationModel.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Klassifikationsmodeller", + "details": { + "scoreInfo": "Scoren repræsenterer den gennemsnitlige klassifikationssikkerhed på tværs af alle registreringer af dette objekt.", + "unknown": "Ukendt" + }, + "description": { + "invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger." + }, + "button": { + "deleteClassificationAttempts": "Slet klassifikationsbilleder", + "renameCategory": "Omdøb klasse", + "deleteCategory": "Slet klasse", + "deleteImages": "Slet billeder", + "trainModel": "Træn model", + "addClassification": "Tilføj klassifikation" + } +} diff --git a/web/public/locales/da/views/configEditor.json b/web/public/locales/da/views/configEditor.json index 0967ef424..ba1d6a715 100644 --- a/web/public/locales/da/views/configEditor.json +++ b/web/public/locales/da/views/configEditor.json @@ -1 +1,10 @@ -{} +{ + "documentTitle": "Konfigurationsstyring - Frigate", + "copyConfig": "Kopiér konfiguration", + "saveAndRestart": "Gem & Genstart", + "saveOnly": "Kun gem", + "configEditor": "Konfigurationseditor", + "safeConfigEditor": "Konfigurationseditor (Sikker tilstand)", + "safeModeDescription": "Frigate er i sikker tilstand på grund af en fejl ved validering af konfigurationen.", + "confirm": "Afslut uden at gemme?" +} diff --git a/web/public/locales/da/views/events.json b/web/public/locales/da/views/events.json index 0967ef424..f59b2f356 100644 --- a/web/public/locales/da/views/events.json +++ b/web/public/locales/da/views/events.json @@ -1 +1,16 @@ -{} +{ + "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" + } +} diff --git a/web/public/locales/da/views/explore.json b/web/public/locales/da/views/explore.json index 0967ef424..afe962aea 100644 --- a/web/public/locales/da/views/explore.json +++ b/web/public/locales/da/views/explore.json @@ -1 +1,29 @@ -{} +{ + "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: " + } + }, + "title": "Udforsk er ikke tilgængelig" + }, + "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 index 0967ef424..8c5f119c4 100644 --- a/web/public/locales/da/views/exports.json +++ b/web/public/locales/da/views/exports.json @@ -1 +1,12 @@ -{} +{ + "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" +} diff --git a/web/public/locales/da/views/faceLibrary.json b/web/public/locales/da/views/faceLibrary.json index 87f3a3437..f309e6fa0 100644 --- a/web/public/locales/da/views/faceLibrary.json +++ b/web/public/locales/da/views/faceLibrary.json @@ -1,3 +1,19 @@ { - "selectItem": "Vælg {{item}}" + "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." + }, + "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}}" + } } diff --git a/web/public/locales/da/views/live.json b/web/public/locales/da/views/live.json index 0967ef424..254539b38 100644 --- a/web/public/locales/da/views/live.json +++ b/web/public/locales/da/views/live.json @@ -1 +1,21 @@ -{} +{ + "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" + } + } + } +} diff --git a/web/public/locales/da/views/recording.json b/web/public/locales/da/views/recording.json index 0967ef424..4028727ac 100644 --- a/web/public/locales/da/views/recording.json +++ b/web/public/locales/da/views/recording.json @@ -1 +1,12 @@ -{} +{ + "filter": "Filter", + "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/search.json b/web/public/locales/da/views/search.json index 0967ef424..1cdc1460b 100644 --- a/web/public/locales/da/views/search.json +++ b/web/public/locales/da/views/search.json @@ -1 +1,12 @@ -{} +{ + "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" + } +} diff --git a/web/public/locales/da/views/settings.json b/web/public/locales/da/views/settings.json index 0967ef424..61fce336f 100644 --- a/web/public/locales/da/views/settings.json +++ b/web/public/locales/da/views/settings.json @@ -1 +1,14 @@ -{} +{ + "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" + } +} diff --git a/web/public/locales/da/views/system.json b/web/public/locales/da/views/system.json index 0967ef424..31d7ac946 100644 --- a/web/public/locales/da/views/system.json +++ b/web/public/locales/da/views/system.json @@ -1 +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 index 0e0e50935..4b1877501 100644 --- a/web/public/locales/de/audio.json +++ b/web/public/locales/de/audio.json @@ -296,7 +296,7 @@ "doorbell": "Türklingel", "ding-dong": "BimBam", "sliding_door": "Schiebetür", - "slam": "Knall", + "slam": "zuknallen", "knock": "Klopfen", "tap": "Schlag", "squeak": "Quietschen", @@ -355,7 +355,7 @@ "shatter": "Zerspringen", "silence": "Stille", "environmental_noise": "Umgebungsgeräusch", - "static": "Rauschen", + "static": "Statisch", "pink_noise": "Rosa Rauschen", "television": "Fernsehgerät", "radio": "Radio", @@ -425,5 +425,79 @@ "sanding": "Schleifen", "machine_gun": "Maschinengewehr", "boom": "Dröhnen", - "field_recording": "Außenaufnahme" + "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 index 8a3eff88c..a9d13566e 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -12,13 +12,13 @@ "24hours": "24 Stunden", "month_one": "{{time}} Monat", "month_other": "{{time}} Monate", - "d": "{{time}} Tag", + "d": "{{time}} Tg.", "day_one": "{{time}} Tag", "day_other": "{{time}} Tage", - "m": "{{time}} Minute", + "m": "{{time}} Min", "minute_one": "{{time}} Minute", "minute_other": "{{time}} Minuten", - "s": "{{time}} Sekunde", + "s": "{{time}}s", "second_one": "{{time}} Sekunde", "second_other": "{{time}} Sekunden", "formattedTimestamp2": { @@ -37,12 +37,12 @@ "30minutes": "30 Minuten", "1hour": "1 Stunde", "lastWeek": "Letzte Woche", - "h": "{{time}} Stunde", - "ago": "{{timeAgo}} her", + "h": "{{time}} Std.", + "ago": "vor {{timeAgo}}", "untilRestart": "Bis zum Neustart", "justNow": "Gerade", "pm": "nachmittags", - "mo": "{{time}}Monat", + "mo": "{{time}} Mon.", "formattedTimestamp": { "12hour": "d. MMM, hh:mm:ss aaa", "24hour": "dd. MMM, hh:mm:ss aaa" @@ -81,7 +81,10 @@ "formattedTimestampMonthDayYear": { "12hour": "d. MMM yyyy", "24hour": "d. MMM yyyy" - } + }, + "inProgress": "Im Gange", + "invalidStartTime": "Ungültige Startzeit", + "invalidEndTime": "Ungültige Endzeit" }, "button": { "save": "Speichern", @@ -107,7 +110,7 @@ "off": "AUS", "reset": "Zurücksetzen", "copy": "Kopieren", - "twoWayTalk": "bidirecktionales Gespräch", + "twoWayTalk": "Zwei-Wege-Kommunikation", "exitFullscreen": "Vollbild verlassen", "unselect": "Selektion aufheben", "copyCoordinates": "Kopiere Koordinaten", @@ -118,10 +121,17 @@ "pictureInPicture": "Bild in Bild", "on": "AN", "suspended": "Pausierte", - "unsuspended": "fortsetzen" + "unsuspended": "fortsetzen", + "continue": "Weiter" }, "label": { - "back": "Zurück" + "back": "Zurück", + "hide": "Verstecke {{item}}", + "show": "Zeige {{item}}", + "ID": "ID", + "none": "Nichts", + "all": "Alle", + "other": "andere" }, "menu": { "configurationEditor": "Konfigurationseditor", @@ -160,7 +170,15 @@ "sk": "Slowakisch", "yue": "粵語 (Kantonesisch)", "th": "ไทย (Thailändisch)", - "ca": "Català (Katalanisch)" + "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)" }, "appearance": "Erscheinung", "theme": { @@ -168,7 +186,7 @@ "blue": "Blau", "green": "Grün", "default": "Standard", - "nord": "Norden", + "nord": "Nord", "red": "Rot", "contrast": "Hoher Kontrast", "highcontrast": "Hoher Kontrast" @@ -214,7 +232,8 @@ "logout": "Abmelden" }, "uiPlayground": "Testgebiet für Benutzeroberfläche", - "export": "Exportieren" + "export": "Exportieren", + "classification": "Klassifizierung" }, "unit": { "speed": { @@ -224,10 +243,18 @@ "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.", + "copyUrlToClipboard": "URL in Zwischenablage kopiert.", "save": { "error": { "title": "Speichern der Konfigurationsänderungen gescheitert: {{errorMessage}}", @@ -240,7 +267,7 @@ "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." + "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": { @@ -260,9 +287,22 @@ "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": ", " } } diff --git a/web/public/locales/de/components/auth.json b/web/public/locales/de/components/auth.json index 8cbd1ff8c..2c4886641 100644 --- a/web/public/locales/de/components/auth.json +++ b/web/public/locales/de/components/auth.json @@ -10,6 +10,7 @@ "unknownError": "Unbekannter Fehler. Prüfe Logs." }, "user": "Benutzername", - "password": "Kennwort" + "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 index fb6f89e74..32874bab6 100644 --- a/web/public/locales/de/components/camera.json +++ b/web/public/locales/de/components/camera.json @@ -58,7 +58,8 @@ "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": { diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json index cedd1c114..464db5adf 100644 --- a/web/public/locales/de/components/dialog.json +++ b/web/public/locales/de/components/dialog.json @@ -66,7 +66,8 @@ "failed": "Fehler beim Starten des Exports: {{error}}", "noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt" }, - "success": "Export erfolgreich gestartet. Die Datei befindet sich im Ordner /exports." + "success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.", + "view": "Ansicht" }, "fromTimeline": { "saveExport": "Export speichern", @@ -117,7 +118,16 @@ "button": { "export": "Exportieren", "markAsReviewed": "Als geprüft markieren", - "deleteNow": "Jetzt löschen" + "deleteNow": "Jetzt löschen", + "markAsUnreviewed": "Als ungeprüft markieren" } + }, + "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 index a2c7db779..d593080cd 100644 --- a/web/public/locales/de/components/filter.json +++ b/web/public/locales/de/components/filter.json @@ -101,7 +101,7 @@ "title": "Lade", "desc": "Wenn das Protokollfenster nach unten gescrollt wird, werden neue Protokolle automatisch geladen, sobald sie hinzugefügt werden." }, - "disableLogStreaming": "Log des Streams deaktvieren", + "disableLogStreaming": "Log des Streams deaktivieren", "allLogs": "Alle Logs" }, "trackedObjectDelete": { @@ -121,6 +121,20 @@ "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." + "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/player.json b/web/public/locales/de/components/player.json index a6b251f01..56a195053 100644 --- a/web/public/locales/de/components/player.json +++ b/web/public/locales/de/components/player.json @@ -24,7 +24,7 @@ "title": "Latenz:", "value": "{{seconds}} Sekunden", "short": { - "title": "Lazenz", + "title": "Latenz", "value": "{{seconds}} s" } }, diff --git a/web/public/locales/de/objects.json b/web/public/locales/de/objects.json index 57fb35617..f3fdbd370 100644 --- a/web/public/locales/de/objects.json +++ b/web/public/locales/de/objects.json @@ -27,7 +27,7 @@ "donut": "Donut", "cake": "Kuchen", "chair": "Stuhl", - "couch": "Couch", + "couch": "Sofa", "bed": "Bett", "dining_table": "Esstisch", "toilet": "Toilette", diff --git a/web/public/locales/de/views/classificationModel.json b/web/public/locales/de/views/classificationModel.json new file mode 100644 index 000000000..2de77e73e --- /dev/null +++ b/web/public/locales/de/views/classificationModel.json @@ -0,0 +1,188 @@ +{ + "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": "Klasse gelöscht", + "deletedImage": "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" + }, + "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." + } + }, + "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 Zustände", + "description": "Es wird empfohlen für alle Zustände Beispiele auszuwählen. Das Modell wird erst trainiert, wenn für alle Zustände Bilder vorhanden sind. Fahren Sie fort und verwenden Sie die Ansicht „Aktuelle Klassifizierungen“, um Bilder für die fehlenden Zustände zu klassifizieren. Trainieren Sie anschließend das Modell." + } + } + }, + "none": "Keiner" +} diff --git a/web/public/locales/de/views/configEditor.json b/web/public/locales/de/views/configEditor.json index 7f975e31b..86959e126 100644 --- a/web/public/locales/de/views/configEditor.json +++ b/web/public/locales/de/views/configEditor.json @@ -12,5 +12,7 @@ } }, "documentTitle": "Konfigurationseditor – Frigate", - "confirm": "Verlassen ohne zu Speichern?" + "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 index 2a38ac029..963482073 100644 --- a/web/public/locales/de/views/events.json +++ b/web/public/locales/de/views/events.json @@ -8,7 +8,11 @@ "empty": { "alert": "Es gibt keine zu prüfenden Alarme", "detection": "Es gibt keine zu prüfenden Erkennungen", - "motion": "Keine Bewegungsdaten gefunden" + "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": "Zeitleiste", "timeline.aria": "Zeitleiste auswählen", @@ -34,5 +38,30 @@ "markAsReviewed": "Als geprüft kennzeichnen", "selected_one": "{{count}} ausgewählt", "selected_other": "{{count}} ausgewählt", - "detected": "erkannt" + "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" } diff --git a/web/public/locales/de/views/explore.json b/web/public/locales/de/views/explore.json index ee518fc11..273c568a2 100644 --- a/web/public/locales/de/views/explore.json +++ b/web/public/locales/de/views/explore.json @@ -17,12 +17,16 @@ "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." + "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}}" + "updatedLPRFailed": "Aktualisierung des Kennzeichens fehlgeschlagen: {{errorMessage}}", + "audioTranscription": "Die Anforderung der Audio Transkription ist fehlgeschlagen: {{errorMessage}}", + "updatedAttributesFailed": "Attribute konnten nicht aktualisiert werden: {{errorMessage}}" } } }, @@ -55,7 +59,7 @@ }, "description": { "label": "Beschreibung", - "placeholder": "Beschreibund des verfolgten Objekts", + "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", @@ -67,6 +71,17 @@ }, "snapshotScore": { "label": "Schnappschuss Bewertung" + }, + "score": { + "label": "Ergebnis" + }, + "editAttributes": { + "title": "Attribute bearbeiten", + "desc": "Wählen Sie Klassifizierungsattribute für dieses {{label}} aus" + }, + "attributes": "Klassifizierungsattribute", + "title": { + "label": "Titel" } }, "documentTitle": "Erkunde - Frigate", @@ -153,7 +168,9 @@ "details": "Details", "video": "Video", "object_lifecycle": "Objekt-Lebenszyklus", - "snapshot": "Snapshot" + "snapshot": "Snapshot", + "thumbnail": "Vorschaubild", + "tracking_details": "Nachverfolgungs-Details" }, "itemMenu": { "downloadSnapshot": { @@ -182,12 +199,34 @@ }, "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" } }, "dialog": { "confirmDelete": { "title": "Löschen bestätigen", - "desc": "Beim Löschen dieses verfolgten Objekts werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Objektlebenszykluseinträge entfernt. Aufgezeichnetes Filmmaterial dieses verfolgten Objekts in der Verlaufsansicht wird NICHT gelöscht.

    Sind Sie sicher, dass Sie fortfahren möchten?" + "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?" } }, "searchResult": { @@ -197,11 +236,68 @@ "error": "Das verfolgte Objekt konnte nicht gelöscht werden: {{errorMessage}}" } }, - "tooltip": "Entspricht {{type}} bei {{confidence}}%" + "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" + "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" + } + }, + "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 index 2fb729cc2..c3bae1239 100644 --- a/web/public/locales/de/views/exports.json +++ b/web/public/locales/de/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Export teilen", + "downloadVideo": "Video herunterladen", + "editName": "Name ändern", + "deleteExport": "Export löschen" } } diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json index 960c555db..1c96176e5 100644 --- a/web/public/locales/de/views/faceLibrary.json +++ b/web/public/locales/de/views/faceLibrary.json @@ -1,7 +1,7 @@ { "description": { "placeholder": "Gib einen Name für diese Kollektion ein", - "addFace": "Anleitung für das Hinzufügen einer neuen Kollektion zur Gesichtsbibliothek.", + "addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem du ein Bild hochlädst.", "invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten." }, "details": { @@ -22,14 +22,14 @@ "title": "Kollektion erstellen", "new": "Lege ein neues Gesicht an", "desc": "Erstelle eine neue Kollektion", - "nextSteps": "Um eine solide Grundlage zu bilden:
  • Benutze den Trainieren 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.
  • " + "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 oder klicke um eines auszuwählen", + "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." @@ -44,9 +44,10 @@ "deleteFace": "Lösche Gesicht" }, "train": { - "title": "Trainiere", - "aria": "Wähle Training", - "empty": "Es gibt keine aktuellen Versuche zurGesichtserkennung" + "title": "Neueste Erkennungen", + "aria": "Wähle aktuelle Erkennungen", + "empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung", + "titleShort": "frisch" }, "deleteFaceLibrary": { "title": "Lösche Name", @@ -64,7 +65,7 @@ "deletedName_other": "{{count}} Gesichter wurden erfolgreich gelöscht.", "addFaceLibrary": "{{name}} wurde erfolgreich in die Gesichtsbibliothek aufgenommen!", "trainedFace": "Gesicht erfolgreich trainiert.", - "updatedFaceScore": "Gesichtsbewertung erfolgreich aktualisiert.", + "updatedFaceScore": "Gesichtsbewertung erfolgreich auf {{name}} ({{score}}) aktualisiert.", "renamedFace": "Gesicht erfolgreich in {{name}} umbenannt" }, "error": { diff --git a/web/public/locales/de/views/live.json b/web/public/locales/de/views/live.json index 318c2b720..e0bf9955e 100644 --- a/web/public/locales/de/views/live.json +++ b/web/public/locales/de/views/live.json @@ -30,16 +30,24 @@ }, "zoom": { "in": { - "label": "PTZ-Kamera vergrößern" + "label": "PTZ-Kamera rein zoomen" }, "out": { - "label": "PTZ-Kamera herauszoomen" + "label": "PTZ-Kamera heraus zoomen" } }, - "presets": "PTZ-Kameravoreinstellungen", + "presets": "PTZ-Kamera Voreinstellungen", "frame": { "center": { - "label": "Klicken Sie in den Rahmen, um die PTZ-Kamera zu zentrieren" + "label": "Klicke in den Rahmen, um die PTZ-Kamera zu zentrieren" + } + }, + "focus": { + "in": { + "label": "PTZ Kamera hinein fokussieren" + }, + "out": { + "label": "PTZ Kamera hinaus fokussieren" } } }, @@ -54,8 +62,8 @@ "enable": "Aufzeichnung aktivieren" }, "snapshots": { - "enable": "Snapshots aktivieren", - "disable": "Snapshots deaktivieren" + "enable": "Schnappschüsse aktivieren", + "disable": "Schnappschüsse deaktivieren" }, "autotracking": { "disable": "Autotracking deaktivieren", @@ -66,7 +74,7 @@ "disable": "Stream-Statistiken ausblenden" }, "manualRecording": { - "title": "On-Demand Aufzeichnung", + "title": "On-Demand", "showStats": { "label": "Statistiken anzeigen", "desc": "Aktivieren Sie diese Option, um Stream-Statistiken als Overlay über dem Kamera-Feed anzuzeigen." @@ -80,7 +88,7 @@ "desc": "Aktivieren Sie diese Option, um das Streaming fortzusetzen, wenn der Player ausgeblendet ist.", "label": "Im Hintergrund abspielen" }, - "tips": "Starten Sie ein manuelles Ereignis basierend auf den Aufzeichnung Aufbewahrungseinstellungen dieser Kamera.", + "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." @@ -100,7 +108,7 @@ "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": "Für diesen Stream ist keine Zwei-Wege-Kommunikation möglich." + "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.", @@ -110,6 +118,9 @@ "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": { @@ -146,7 +157,8 @@ "cameraEnabled": "Kamera aktiviert", "autotracking": "Autotracking", "audioDetection": "Audioerkennung", - "title": "{{camera}} Einstellungen" + "title": "{{camera}} Einstellungen", + "transcription": "Audio Transkription" }, "history": { "label": "Historisches Filmmaterial zeigen" @@ -154,5 +166,24 @@ "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." + } + }, + "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/search.json b/web/public/locales/de/views/search.json index c3800ab28..0b6424f42 100644 --- a/web/public/locales/de/views/search.json +++ b/web/public/locales/de/views/search.json @@ -25,7 +25,8 @@ "max_speed": "Maximalgeschwindigkeit", "time_range": "Zeitraum", "labels": "Labels", - "sub_labels": "Unterlabels" + "sub_labels": "Unterlabels", + "attributes": "Attribute" }, "toast": { "error": { @@ -58,7 +59,7 @@ "title": "Wie man Textfilter verwendet" }, "searchType": { - "thumbnail": "Miniaturansicht", + "thumbnail": "Vorschaubild", "description": "Beschreibung" } }, diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json index 29c5d6ece..4f10f2f51 100644 --- a/web/public/locales/de/views/settings.json +++ b/web/public/locales/de/views/settings.json @@ -3,26 +3,32 @@ "default": "Einstellungen - Frigate", "authentication": "Authentifizierungseinstellungen – Frigate", "camera": "Kameraeinstellungen - Frigate", - "masksAndZones": "Masken- und Zonen-Editor – Frigate", + "masksAndZones": "Masken- und Zoneneditor – Frigate", "object": "Debug - Frigate", - "general": "Allgemeine Einstellungen – Frigate", + "general": "UI-Einstellungen - Frigate", "frigatePlus": "Frigate+ Einstellungen – Frigate", "classification": "Klassifizierungseinstellungen – Frigate", - "motionTuner": "Bewegungstuner – Frigate", - "notifications": "Benachrichtigungs-Einstellungen", - "enrichments": "Erweiterte Statistiken - Frigate" + "motionTuner": "Bewegungserkennungs-Optimierer – Frigate", + "notifications": "Benachrichtigungseinstellungen", + "enrichments": "Erweiterte Statistiken - Frigate", + "cameraManagement": "Kameras verwalten - Frigate", + "cameraReview": "Kameraeinstellungen prüfen - Frigate" }, "menu": { "ui": "Benutzeroberfläche", "cameras": "Kameraeinstellungen", "classification": "Klassifizierung", "masksAndZones": "Maskierungen / Zonen", - "motionTuner": "Bewegungstuner", + "motionTuner": "Bewegungserkennungs-Optimierer", "debug": "Debug", "frigateplus": "Frigate+", "users": "Benutzer", "notifications": "Benachrichtigungen", - "enrichments": "Verbesserungen" + "enrichments": "Erkennungsfunktionen", + "triggers": "Auslöser", + "roles": "Rollen", + "cameraManagement": "Verwaltung", + "cameraReview": "Überprüfung" }, "dialog": { "unsavedChanges": { @@ -35,7 +41,7 @@ "noCamera": "Keine Kamera" }, "general": { - "title": "Allgemeine Einstellungen", + "title": "Einstellungen der Benutzeroberfläche", "liveDashboard": { "title": "Live Übersicht", "playAlertVideos": { @@ -43,8 +49,16 @@ "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 einen Aktivität erkannt wurde. Wenn du diese Option deaktivierst, werden die statischen Kamerabilder auf der Liveübersicht nur einmal pro Minute aktualisiert.", + "desc": "Zeigt automatisch das Live-Bild einer Kamera an, wenn eine Aktivität erkannt wird. Ist diese Option deaktiviert, werden Kamerabilder im Live-Dashboard nur einmal pro Minute aktualisiert.", "label": "Automatische Live Ansicht" + }, + "displayCameraNames": { + "label": "Immer Namen der Kamera anzeigen", + "desc": "Kamerabezeichnung immer im einem Chip im Live-View-Dashboard für mehrere 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": { @@ -68,7 +82,7 @@ "title": "Kalender", "firstWeekday": { "label": "Erster Wochentag", - "desc": "Der Tag, an dem die Wochen des Review Kalenders beginnen.", + "desc": "Der Tag, an dem die Wochen des Überprüfungs-Kalenders beginnen.", "sunday": "Sonntag", "monday": "Montag" } @@ -178,7 +192,45 @@ "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": "Kamera-Einstellungen" + "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": { @@ -188,7 +240,8 @@ "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." + "mustNotContainPeriod": "Der Zonenname darf keine Punkte enthalten.", + "mustHaveAtLeastOneLetter": "Der Name der Zone muss mindestens einen Buchstaben enthalten." } }, "loiteringTime": { @@ -245,7 +298,7 @@ "zones": { "edit": "Zone bearbeiten", "toast": { - "success": "Die Zone ({{zoneName}}) wurde gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen." + "success": "Die Zone ({{zoneName}}) wurde gespeichert." }, "desc": { "documentation": "Dokumentation", @@ -267,7 +320,7 @@ "name": { "title": "Name", "inputPlaceHolder": "Geben Sie einen Namen ein…", - "tips": "Der Name muss aus mindestens 2 Zeichen bestehen und sollte nicht den Namen einer Kamera oder anderen Zone entsprechen." + "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", @@ -308,8 +361,8 @@ "clickDrawPolygon": "Klicke, um ein Polygon auf dem Bild zu zeichnen.", "toast": { "success": { - "noName": "Bewegungsmaske wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen.", - "title": "{{polygonName}} wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen." + "noName": "Bewegungsmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." } }, "add": "Neue Bewegungsmaske", @@ -329,8 +382,8 @@ "documentTitle": "Objektmaske bearbeiten – Frigate", "toast": { "success": { - "noName": "Objektmaske wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen.", - "title": "{{polygonName}} wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen." + "noName": "Objektmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." } }, "desc": { @@ -397,7 +450,20 @@ "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." + "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": { @@ -420,11 +486,11 @@ "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 am Bewegungstuner ({{camera}})" + "unsavedChanges": "Nicht gespeicherte Änderungen im Bewegungserkennungs-Optimierer ({{camera}})" }, "users": { "addUser": "Benutzer hinzufügen", - "updatePassword": "Passwort aktualisieren", + "updatePassword": "Passwort zurücksetzen", "toast": { "success": { "deleteUser": "Benutzer {{user}} wurde erfolgreich gelöscht", @@ -448,7 +514,7 @@ "changeRole": "Benutzerrolle ändern", "deleteUser": "Benutzer löschen", "noUsers": "Keine Benutzer gefunden.", - "password": "Passwort", + "password": "Passwort zurücksetzen", "username": "Benutzername", "actions": "Aktionen", "role": "Rolle" @@ -475,7 +541,16 @@ }, "match": "Passwörter stimmen überein", "title": "Passwort", - "placeholder": "Passwort eingeben" + "placeholder": "Passwort eingeben", + "requirements": { + "title": "Passwort Anforderungen:", + "length": "Mindestens 8 Zeichen", + "uppercase": "Mindestens ein Großbuchstabe", + "digit": "Mindestens eine Ziffer", + "special": "Mindestens ein Sonderzeichen (!@#$%^&*(),.?\":{}|<>)" + }, + "show": "Passwort anzeigen", + "hide": "Verberge Passwort" }, "newPassword": { "title": "Neues Passwort", @@ -485,7 +560,11 @@ } }, "usernameIsRequired": "Benutzername ist erforderlich", - "passwordIsRequired": "Passwort benötigt" + "passwordIsRequired": "Passwort benötigt", + "currentPassword": { + "title": "Aktuelles Passwort", + "placeholder": "Gib Dein aktuelles Passwort ein" + } }, "changeRole": { "desc": "Berechtigungen für {{username}} aktualisieren", @@ -494,7 +573,8 @@ "admin": "Admin", "adminDesc": "Voller Zugang zu allen Funktionen.", "viewer": "Betrachter", - "viewerDesc": "Nur auf Live-Dashboards, Überprüfung, Erkundung und Exporte beschränkt." + "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" @@ -515,7 +595,12 @@ "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" + "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." } } }, @@ -620,21 +705,21 @@ }, "enrichments": { "birdClassification": { - "title": "Vogel Klassifizierung", - "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": "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": "Anreicherungseinstellungen", "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 indizien", + "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": "Neuinszenierung bestätigen", + "confirmTitle": "Neuindizierung bestätigen", "confirmButton": "Neuindizierung", "success": "Die Neuindizierung wurde erfolgreich gestartet.", "alreadyInProgress": "Die Neuindizierung ist bereits im Gange.", - "error": "Neuindizierung konnte nicht gestartet werden: {{errorMessage}}" + "error": "Die Neuindizierung konnte nicht gestartet werden: {{errorMessage}}" }, "modelSize": { "small": { @@ -645,7 +730,7 @@ "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 zutreffend." + "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", @@ -654,10 +739,10 @@ }, "faceRecognition": { "title": "Gesichtserkennung", - "desc": "Die Gesichtserkennung ermöglicht es, Personen Namen zuzuweisen, und wenn ihr 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.", + "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": "Modell Größe", + "label": "Modellgröße", "desc": "Die Größe des für die Gesichtserkennung verwendeten Modells.", "small": { "title": "klein", @@ -679,5 +764,538 @@ "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://“ 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 Reolink-HTTP-Streams sollten sie FFmpeg verwenden, um eine bessere Kompatibilität zu gewährleisten. Aktivieren Sie für diesen Stream die Option „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." + } + } + } + }, + "cameraManagement": { + "title": "Kameras verwalten", + "addCamera": "Neue Kamera hinzufügen", + "editCamera": "Kamera bearbeiten:", + "selectCamera": "Wähle eine Kamera", + "backToSettings": "Zurück zu Kameraeinstellungen", + "streams": { + "title": "Kameras aktivieren / deaktivieren", + "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." + }, + "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" + } + } + }, + "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": "Generative KI Review Beschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Option deaktiviert ist, werden für die Review Elemente 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." + } + } } } diff --git a/web/public/locales/de/views/system.json b/web/public/locales/de/views/system.json index f869f1ba2..0437c65b1 100644 --- a/web/public/locales/de/views/system.json +++ b/web/public/locales/de/views/system.json @@ -16,7 +16,7 @@ "vbios": "VBios Info: {{vbios}}" }, "closeInfo": { - "label": "Schhließe GPU Info" + "label": "Schließe GPU Info" }, "copyInfo": { "label": "Kopiere GPU Info" @@ -31,7 +31,12 @@ "gpuDecoder": "GPU Decoder", "gpuEncoder": "GPU Encoder", "npuUsage": "NPU Verwendung", - "npuMemory": "NPU Speicher" + "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." + } }, "title": "Allgemein", "detector": { @@ -39,12 +44,20 @@ "cpuUsage": "CPU-Auslastung des Detektors", "memoryUsage": "Arbeitsspeichernutzung des Detektors", "inferenceSpeed": "Detektoren Inferenzgeschwindigkeit", - "temperature": "Temperatur des Detektors" + "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" + "processMemoryUsage": "Prozessspeicherauslastung", + "series": { + "go2rtc": "go2rtc", + "recording": "Aufnahme", + "audio_detector": "Geräuscherkennung", + "review_segment": "Überprüfungsteil", + "embeddings": "Einbettungen" + } } }, "documentTitle": { @@ -102,7 +115,11 @@ "bandwidth": "Bandbreite" }, "title": "Speicher", - "overview": "Übersicht" + "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." + } }, "cameras": { "info": { @@ -115,7 +132,7 @@ "unknown": "Unbekannt", "audio": "Audio:", "error": "Fehler: {{error}}", - "cameraProbeInfo": "{{camera}} Kamera-Untersuchsungsinfo", + "cameraProbeInfo": "{{camera}} Kamera-Untersuchungsinfo", "streamDataFromFFPROBE": "Stream-Daten werden mit ffprobe erhalten.", "tips": { "title": "Kamera-Untersuchsungsinfo" @@ -162,10 +179,20 @@ "face_recognition": "Gesichts Erkennung", "image_embedding": "Bild Embedding", "yolov9_plate_detection_speed": "YOLOv9 Kennzeichenerkennungsgeschwindigkeit", - "yolov9_plate_detection": "YOLOv9 Kennzeichenerkennung" + "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" + "infPerSecond": "Rückschlüsse pro Sekunde", + "averageInf": "Durchschnittliche Inferenzzeit" }, "stats": { "healthy": "Das System läuft problemlos", @@ -174,7 +201,8 @@ "reindexingEmbeddings": "Neuindizierung von Einbettungen ({{processed}}% erledigt)", "detectIsSlow": "{{detect}} ist langsam ({{speed}} ms)", "detectIsVerySlow": "{{detect}} ist sehr langsam ({{speed}} ms)", - "cameraIsOffline": "{{camera}} ist offline" + "cameraIsOffline": "{{camera}} ist offline", + "shmTooLow": "Die Zuweisung für /dev/shm ({{total}} MB) sollte auf mindestens {{min}} MB erhöht werden." }, "lastRefreshed": "Zuletzt aktualisiert: " } diff --git a/web/public/locales/el/audio.json b/web/public/locales/el/audio.json index f8dfffbc8..2bd01b871 100644 --- a/web/public/locales/el/audio.json +++ b/web/public/locales/el/audio.json @@ -48,5 +48,18 @@ "acoustic_guitar": "Ακουστική Κιθάρα", "classical_music": "Κλασική Μουσική", "opera": "Όπερα", - "electronic_music": "Ηλεκτρονική Μουσική" + "electronic_music": "Ηλεκτρονική Μουσική", + "bus": "Λεωφορείο", + "train": "Εκπαίδευση", + "boat": "Βάρκα", + "sigh": "Αναστεναγμός", + "singing": "Τραγούδι", + "choir": "Χορωδία", + "whistling": "Σφύριγμα", + "camera": "Κάμερα", + "wheeze": "Ξεφύσημα", + "yodeling": "Λαρυγγισμός", + "chant": "Ύμνος", + "mantra": "Μάντρα", + "synthetic_singing": "Συνθετικό Τραγούδι" } diff --git a/web/public/locales/el/common.json b/web/public/locales/el/common.json index d521af9a0..3bcd3316f 100644 --- a/web/public/locales/el/common.json +++ b/web/public/locales/el/common.json @@ -1,8 +1,181 @@ { "time": { - "untilForTime": "Ως{{time}}", + "untilForTime": "Ως {{time}}", "untilForRestart": "Μέχρι να γίνει επανεκίννηση του Frigate.", "untilRestart": "Μέχρι να γίνει επανεκκίνηση", - "justNow": "Μόλις τώρα" + "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": "Μη έγκυρη ώρα λήξης" + }, + "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": "Ιστορία" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "χλμ/ώρα" + }, + "length": { + "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": "Όλα" + }, + "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 index 722e8efbf..c978b3667 100644 --- a/web/public/locales/el/components/auth.json +++ b/web/public/locales/el/components/auth.json @@ -4,7 +4,13 @@ "password": "Κωδικός", "login": "Σύνδεση", "errors": { - "usernameRequired": "Απαιτείται όνομα χρήστη" - } + "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 index 8d0571fbe..3de7248ee 100644 --- a/web/public/locales/el/components/camera.json +++ b/web/public/locales/el/components/camera.json @@ -1,6 +1,42 @@ { "group": { "add": "Προσθήκη ομάδας καμερών", - "label": "Ομάδες καμερών" + "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 index 5d83ef580..40c3f0545 100644 --- a/web/public/locales/el/components/dialog.json +++ b/web/public/locales/el/components/dialog.json @@ -32,7 +32,24 @@ }, "export": { "time": { - "fromTimeline": "Επιλογή από Χρονολόγιο" + "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 index ecfa4905e..da69d7f0e 100644 --- a/web/public/locales/el/components/filter.json +++ b/web/public/locales/el/components/filter.json @@ -1,6 +1,41 @@ { "filter": "Φίλτρο", "labels": { - "label": "Ετικέτες" - } + "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/player.json b/web/public/locales/el/components/player.json index 14f444437..de23a9783 100644 --- a/web/public/locales/el/components/player.json +++ b/web/public/locales/el/components/player.json @@ -37,6 +37,15 @@ "value": "{{droppedFrames}} καρέ" } }, - "decodedFrames": "Αποκωδικοποιημένα Καρέ:" + "decodedFrames": "Αποκωδικοποιημένα Καρέ:", + "droppedFrameRate": "Ρυθμός Απορριφθέντων Καρέ:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Επιτυχής αποστολή εικόνας στο Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Αποτυχία αποστολής εικόνας στο Frigate+" + } } } diff --git a/web/public/locales/el/objects.json b/web/public/locales/el/objects.json index 22d5874ca..5cc7e4fe8 100644 --- a/web/public/locales/el/objects.json +++ b/web/public/locales/el/objects.json @@ -4,5 +4,21 @@ "car": "Αυτοκίνητο", "motorcycle": "Μηχανή", "airplane": "Αεροπλάνο", - "bird": "Πουλί" + "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/classificationModel.json b/web/public/locales/el/views/classificationModel.json new file mode 100644 index 000000000..3f7b88c7c --- /dev/null +++ b/web/public/locales/el/views/classificationModel.json @@ -0,0 +1,7 @@ +{ + "documentTitle": "Μοντέλα Ταξινόμησης - Frigate", + "details": { + "scoreInfo": "Η βαθμολογία αντιπροσωπεύει την κατά μέσο όρο ταξινομική εμπιστοσύνη μεταξύ όλων των ανιχνεύσεων αυτού του αντικειμένου.", + "none": "Καμία" + } +} diff --git a/web/public/locales/el/views/configEditor.json b/web/public/locales/el/views/configEditor.json index d468103fa..79917bf96 100644 --- a/web/public/locales/el/views/configEditor.json +++ b/web/public/locales/el/views/configEditor.json @@ -1,5 +1,18 @@ { "documentTitle": "Επεξεργαστής ρυθμίσεων - Frigate", "configEditor": "Επεξεργαστής Ρυθμίσεων", - "saveAndRestart": "Αποθήκευση και επανεκκίνηση" + "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 index 76dc0264a..e2e21a05c 100644 --- a/web/public/locales/el/views/events.json +++ b/web/public/locales/el/views/events.json @@ -4,5 +4,29 @@ "motion": { "label": "Κίνηση", "only": "Κίνηση μόνο" - } + }, + "allCameras": "Όλες οι κάμερες", + "empty": { + "alert": "Δεν υπάρχουν ειδοποιήσεις για εξέταση", + "detection": "Δεν υπάρχουν εντοπισμοί για εξέταση", + "motion": "Δεν βρέθηκαν στοιχεία κίνησης" + }, + "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 index a48e770ea..a33fb17bf 100644 --- a/web/public/locales/el/views/explore.json +++ b/web/public/locales/el/views/explore.json @@ -1,3 +1,52 @@ { - "documentTitle": "Εξερευνήστε - Frigate" + "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 index e8517ae5c..f6526eea0 100644 --- a/web/public/locales/el/views/exports.json +++ b/web/public/locales/el/views/exports.json @@ -1,5 +1,22 @@ { "documentTitle": "Εξαγωγή - Frigate", "search": "Αναζήτηση", - "deleteExport": "Διαγραφή εξαγωγής" + "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 index 8ee6c9690..7bb548e07 100644 --- a/web/public/locales/el/views/faceLibrary.json +++ b/web/public/locales/el/views/faceLibrary.json @@ -1,10 +1,50 @@ { "description": { - "addFace": "Οδηγός για την προσθήκη μιας νέας συλλογής στη Βιβλιοθήκη Προσώπων.", + "addFace": "Προσθέστε μια νέα συλλογή στη Βιβλιοθήκη Προσώπων ανεβάζοντας την πρώτη σας εικόνα.", "placeholder": "Εισαγάγετε ένα όνομα για αυτήν τη συλλογή", "invalidName": "Μη έγκυρο όνομα. Τα ονόματα μπορούν να περιλαμβάνουν γράμματα, αριθμούς, κενό διάστημα, απόστροφο, παύλα, κάτω παύλα." }, "details": { - "person": "Άτομο" + "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 index daeb09636..b2427114e 100644 --- a/web/public/locales/el/views/live.json +++ b/web/public/locales/el/views/live.json @@ -1,6 +1,69 @@ { "documentTitle": "Ζωντανά - Frigate", "twoWayTalk": { - "enable": "Ενεργοποίηση αμφίδρομης επικοινωνίας" + "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/recording.json b/web/public/locales/el/views/recording.json index 063abbd2b..9681d0e2a 100644 --- a/web/public/locales/el/views/recording.json +++ b/web/public/locales/el/views/recording.json @@ -2,5 +2,11 @@ "filter": "Φίλτρο", "export": "Εξαγωγή", "calendar": "Ημερολόγιο", - "filters": "Φίλτρα" + "filters": "Φίλτρα", + "toast": { + "error": { + "noValidTimeSelected": "Μη επιλογή έγκυρης περιόδου", + "endTimeMustAfterStartTime": "Το επιλεγμένο τέλος περιόδου πρέπει να είναι μετά την επιλεγμένη αρχή περιόδου" + } + } } diff --git a/web/public/locales/el/views/search.json b/web/public/locales/el/views/search.json index 96ca56e0d..1281446cf 100644 --- a/web/public/locales/el/views/search.json +++ b/web/public/locales/el/views/search.json @@ -2,6 +2,28 @@ "search": "Αναζήτηση", "savedSearches": "Αποθηκευμένες Αναζητήσεις", "button": { - "clear": "Εκαθάρηση αναζήτησης" + "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 index 0d184f3d2..75884e0d2 100644 --- a/web/public/locales/el/views/settings.json +++ b/web/public/locales/el/views/settings.json @@ -2,6 +2,57 @@ "documentTitle": { "default": "Ρυθμίσεις - Frigate", "authentication": "Ρυθμίσεις ελέγχου ταυτοποίησης - Frigate", - "camera": "Ρυθμίσεις Κάμερας - 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 index 3076645d6..0ec8ff587 100644 --- a/web/public/locales/el/views/system.json +++ b/web/public/locales/el/views/system.json @@ -1,5 +1,39 @@ { "documentTitle": { - "cameras": "Στατιστικά Καμερών - Frigate" + "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 index de5f5638c..5c197e85b 100644 --- a/web/public/locales/en/audio.json +++ b/web/public/locales/en/audio.json @@ -425,5 +425,79 @@ "television": "Television", "radio": "Radio", "field_recording": "Field Recording", - "scream": "Scream" + "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 index 86304fff3..8bf13ca61 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -3,6 +3,7 @@ "untilForTime": "Until {{time}}", "untilForRestart": "Until Frigate restarts.", "untilRestart": "Until restart", + "never": "Never", "ago": "{{timeAgo}} ago", "justNow": "Just now", "today": "Today", @@ -72,7 +73,10 @@ "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": { @@ -82,10 +86,33 @@ "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" + "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": { "apply": "Apply", @@ -122,7 +149,8 @@ "unselect": "Unselect", "export": "Export", "deleteNow": "Delete Now", - "next": "Next" + "next": "Next", + "continue": "Continue" }, "menu": { "system": "System", @@ -215,6 +243,7 @@ "export": "Export", "uiPlayground": "UI Playground", "faceLibrary": "Face Library", + "classification": "Classification", "user": { "title": "User", "account": "Account", @@ -262,5 +291,9 @@ "title": "404", "desc": "Page not found" }, - "selectItem": "Select {{item}}" + "selectItem": "Select {{item}}", + "readTheDocumentation": "Read the documentation", + "information": { + "pixels": "{{area}}px" + } } diff --git a/web/public/locales/en/components/auth.json b/web/public/locales/en/components/auth.json index 05c2a779f..56b750070 100644 --- a/web/public/locales/en/components/auth.json +++ b/web/public/locales/en/components/auth.json @@ -3,6 +3,7 @@ "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", diff --git a/web/public/locales/en/components/camera.json b/web/public/locales/en/components/camera.json index 10513a729..864efa6c4 100644 --- a/web/public/locales/en/components/camera.json +++ b/web/public/locales/en/components/camera.json @@ -27,6 +27,7 @@ "icon": "Icon", "success": "Camera group ({{name}}) has been saved.", "camera": { + "birdseye": "Birdseye", "setting": { "label": "Camera Streaming Settings", "title": "{{cameraName}} Streaming Settings", @@ -35,8 +36,7 @@ "audioIsUnavailable": "Audio is unavailable for this stream", "audio": { "tips": { - "title": "Audio must be output from your camera and configured in go2rtc for this stream.", - "document": "Read the documentation " + "title": "Audio must be output from your camera and configured in go2rtc for this stream." } }, "stream": "Stream", diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json index 8b2dc0b88..a56c2b1da 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -52,7 +52,8 @@ "export": "Export", "selectOrExport": "Select or Export", "toast": { - "success": "Successfully started export. View the file in the /exports folder.", + "success": "Successfully started export. View the file in the exports page.", + "view": "View", "error": { "failed": "Failed to start export: {{error}}", "endTimeMustAfterStartTime": "End time must be after start time", @@ -69,8 +70,7 @@ "restreaming": { "disabled": "Restreaming is not enabled for this camera.", "desc": { - "title": "Set up go2rtc for additional live view options and audio for this camera.", - "readTheDocumentation": "Read the documentation" + "title": "Set up go2rtc for additional live view options and audio for this camera." } }, "showStats": { @@ -107,7 +107,16 @@ "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 index 08a0ee2b2..e9ae5c769 100644 --- a/web/public/locales/en/components/filter.json +++ b/web/public/locales/en/components/filter.json @@ -1,5 +1,11 @@ { "filter": "Filter", + "classes": { + "label": "Classes", + "all": { "title": "All Classes" }, + "count_one": "{{count}} Class", + "count_other": "{{count}} Classes" + }, "labels": { "label": "Labels", "all": { @@ -32,6 +38,10 @@ "label": "Sub Labels", "all": "All Sub Labels" }, + "attributes": { + "label": "Classification Attributes", + "all": "All Attributes" + }, "score": "Score", "estimatedSpeed": "Estimated Speed ({{unit}})", "features": { @@ -121,6 +131,8 @@ "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." + "selectPlatesFromList": "Select one or more plates from the list.", + "selectAll": "Select all", + "clearAll": "Clear all" } } diff --git a/web/public/locales/en/config/audio.json b/web/public/locales/en/config/audio.json new file mode 100644 index 000000000..f9aaffa6b --- /dev/null +++ b/web/public/locales/en/config/audio.json @@ -0,0 +1,26 @@ +{ + "label": "Global Audio events configuration.", + "properties": { + "enabled": { + "label": "Enable audio events." + }, + "max_not_heard": { + "label": "Seconds of not hearing the type of audio to end the event." + }, + "min_volume": { + "label": "Min volume required to run audio detection." + }, + "listen": { + "label": "Audio to listen for." + }, + "filters": { + "label": "Audio filters." + }, + "enabled_in_config": { + "label": "Keep track of original state of audio detection." + }, + "num_threads": { + "label": "Number of detection threads" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/audio_transcription.json b/web/public/locales/en/config/audio_transcription.json new file mode 100644 index 000000000..6922b9d80 --- /dev/null +++ b/web/public/locales/en/config/audio_transcription.json @@ -0,0 +1,23 @@ +{ + "label": "Audio transcription config.", + "properties": { + "enabled": { + "label": "Enable audio transcription." + }, + "language": { + "label": "Language abbreviation to use for audio event transcription/translation." + }, + "device": { + "label": "The device used for license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + }, + "live_enabled": { + "label": "Enable live transcriptions." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/auth.json b/web/public/locales/en/config/auth.json new file mode 100644 index 000000000..a524d8d1b --- /dev/null +++ b/web/public/locales/en/config/auth.json @@ -0,0 +1,35 @@ +{ + "label": "Auth configuration.", + "properties": { + "enabled": { + "label": "Enable authentication" + }, + "reset_admin_password": { + "label": "Reset the admin password on startup" + }, + "cookie_name": { + "label": "Name for jwt token cookie" + }, + "cookie_secure": { + "label": "Set secure flag on cookie" + }, + "session_length": { + "label": "Session length for jwt session tokens" + }, + "refresh_time": { + "label": "Refresh the session if it is going to expire in this many seconds" + }, + "failed_login_rate_limit": { + "label": "Rate limits for failed login attempts." + }, + "trusted_proxies": { + "label": "Trusted proxies for determining IP address to rate limit" + }, + "hash_iterations": { + "label": "Password hash iterations" + }, + "roles": { + "label": "Role to camera mappings. Empty list grants access to all cameras." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/birdseye.json b/web/public/locales/en/config/birdseye.json new file mode 100644 index 000000000..f122f314c --- /dev/null +++ b/web/public/locales/en/config/birdseye.json @@ -0,0 +1,37 @@ +{ + "label": "Birdseye configuration.", + "properties": { + "enabled": { + "label": "Enable birdseye view." + }, + "mode": { + "label": "Tracking mode." + }, + "restream": { + "label": "Restream birdseye via RTSP." + }, + "width": { + "label": "Birdseye width." + }, + "height": { + "label": "Birdseye height." + }, + "quality": { + "label": "Encoding quality." + }, + "inactivity_threshold": { + "label": "Birdseye Inactivity Threshold" + }, + "layout": { + "label": "Birdseye Layout Config", + "properties": { + "scaling_factor": { + "label": "Birdseye Scaling Factor" + }, + "max_cameras": { + "label": "Max cameras" + } + } + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/camera_groups.json b/web/public/locales/en/config/camera_groups.json new file mode 100644 index 000000000..2900e9c67 --- /dev/null +++ b/web/public/locales/en/config/camera_groups.json @@ -0,0 +1,14 @@ +{ + "label": "Camera group configuration", + "properties": { + "cameras": { + "label": "List of cameras in this group." + }, + "icon": { + "label": "Icon that represents camera group." + }, + "order": { + "label": "Sort order for group." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json new file mode 100644 index 000000000..67015bde5 --- /dev/null +++ b/web/public/locales/en/config/cameras.json @@ -0,0 +1,761 @@ +{ + "label": "Camera configuration.", + "properties": { + "name": { + "label": "Camera name." + }, + "friendly_name": { + "label": "Camera friendly name used in the Frigate UI." + }, + "enabled": { + "label": "Enable camera." + }, + "audio": { + "label": "Audio events configuration.", + "properties": { + "enabled": { + "label": "Enable audio events." + }, + "max_not_heard": { + "label": "Seconds of not hearing the type of audio to end the event." + }, + "min_volume": { + "label": "Min volume required to run audio detection." + }, + "listen": { + "label": "Audio to listen for." + }, + "filters": { + "label": "Audio filters." + }, + "enabled_in_config": { + "label": "Keep track of original state of audio detection." + }, + "num_threads": { + "label": "Number of detection threads" + } + } + }, + "audio_transcription": { + "label": "Audio transcription config.", + "properties": { + "enabled": { + "label": "Enable audio transcription." + }, + "language": { + "label": "Language abbreviation to use for audio event transcription/translation." + }, + "device": { + "label": "The device used for license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + }, + "live_enabled": { + "label": "Enable live transcriptions." + } + } + }, + "birdseye": { + "label": "Birdseye camera configuration.", + "properties": { + "enabled": { + "label": "Enable birdseye view for camera." + }, + "mode": { + "label": "Tracking mode for camera." + }, + "order": { + "label": "Position of the camera in the birdseye view." + } + } + }, + "detect": { + "label": "Object detection configuration.", + "properties": { + "enabled": { + "label": "Detection Enabled." + }, + "height": { + "label": "Height of the stream for the detect role." + }, + "width": { + "label": "Width of the stream for the detect role." + }, + "fps": { + "label": "Number of frames per second to process through detection." + }, + "min_initialized": { + "label": "Minimum number of consecutive hits for an object to be initialized by the tracker." + }, + "max_disappeared": { + "label": "Maximum number of frames the object can disappear before detection ends." + }, + "stationary": { + "label": "Stationary objects config.", + "properties": { + "interval": { + "label": "Frame interval for checking stationary objects." + }, + "threshold": { + "label": "Number of frames without a position change for an object to be considered stationary" + }, + "max_frames": { + "label": "Max frames for stationary objects.", + "properties": { + "default": { + "label": "Default max frames." + }, + "objects": { + "label": "Object specific max frames." + } + } + }, + "classifier": { + "label": "Enable visual classifier for determing if objects with jittery bounding boxes are stationary." + } + } + }, + "annotation_offset": { + "label": "Milliseconds to offset detect annotations by." + } + } + }, + "face_recognition": { + "label": "Face recognition config.", + "properties": { + "enabled": { + "label": "Enable face recognition." + }, + "min_area": { + "label": "Min area of face box to consider running face recognition." + } + } + }, + "ffmpeg": { + "label": "FFmpeg configuration for the camera.", + "properties": { + "path": { + "label": "FFmpeg path" + }, + "global_args": { + "label": "Global FFmpeg arguments." + }, + "hwaccel_args": { + "label": "FFmpeg hardware acceleration arguments." + }, + "input_args": { + "label": "FFmpeg input arguments." + }, + "output_args": { + "label": "FFmpeg output arguments per role.", + "properties": { + "detect": { + "label": "Detect role FFmpeg output arguments." + }, + "record": { + "label": "Record role FFmpeg output arguments." + } + } + }, + "retry_interval": { + "label": "Time in seconds to wait before FFmpeg retries connecting to the camera." + }, + "apple_compatibility": { + "label": "Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players." + }, + "inputs": { + "label": "Camera inputs." + } + } + }, + "live": { + "label": "Live playback settings.", + "properties": { + "streams": { + "label": "Friendly names and restream names to use for live view." + }, + "height": { + "label": "Live camera view height" + }, + "quality": { + "label": "Live camera view quality" + } + } + }, + "lpr": { + "label": "LPR config.", + "properties": { + "enabled": { + "label": "Enable license plate recognition." + }, + "expire_time": { + "label": "Expire plates not seen after number of seconds (for dedicated LPR cameras only)." + }, + "min_area": { + "label": "Minimum area of license plate to begin running recognition." + }, + "enhancement": { + "label": "Amount of contrast adjustment and denoising to apply to license plate images before recognition." + } + } + }, + "motion": { + "label": "Motion detection configuration.", + "properties": { + "enabled": { + "label": "Enable motion on all cameras." + }, + "threshold": { + "label": "Motion detection threshold (1-255)." + }, + "lightning_threshold": { + "label": "Lightning detection threshold (0.3-1.0)." + }, + "improve_contrast": { + "label": "Improve Contrast" + }, + "contour_area": { + "label": "Contour Area" + }, + "delta_alpha": { + "label": "Delta Alpha" + }, + "frame_alpha": { + "label": "Frame Alpha" + }, + "frame_height": { + "label": "Frame Height" + }, + "mask": { + "label": "Coordinates polygon for the motion mask." + }, + "mqtt_off_delay": { + "label": "Delay for updating MQTT with no motion detected." + }, + "enabled_in_config": { + "label": "Keep track of original state of motion detection." + } + } + }, + "objects": { + "label": "Object configuration.", + "properties": { + "track": { + "label": "Objects to track." + }, + "filters": { + "label": "Object filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "mask": { + "label": "Object mask." + }, + "genai": { + "label": "Config for using genai to analyze objects.", + "properties": { + "enabled": { + "label": "Enable GenAI for camera." + }, + "use_snapshot": { + "label": "Use snapshots for generating descriptions." + }, + "prompt": { + "label": "Default caption prompt." + }, + "object_prompts": { + "label": "Object specific prompts." + }, + "objects": { + "label": "List of objects to run generative AI for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to run generative AI." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "send_triggers": { + "label": "What triggers to use to send frames to generative AI for a tracked object.", + "properties": { + "tracked_object_end": { + "label": "Send once the object is no longer tracked." + }, + "after_significant_updates": { + "label": "Send an early request to generative AI when X frames accumulated." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + } + } + } + } + }, + "record": { + "label": "Record configuration.", + "properties": { + "enabled": { + "label": "Enable record on all cameras." + }, + "sync_recordings": { + "label": "Sync recordings with disk on startup and once a day." + }, + "expire_interval": { + "label": "Number of minutes to wait between cleanup runs." + }, + "continuous": { + "label": "Continuous recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "motion": { + "label": "Motion recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "detections": { + "label": "Detection specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "alerts": { + "label": "Alert specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "export": { + "label": "Recording Export Config", + "properties": { + "timelapse_args": { + "label": "Timelapse Args" + } + } + }, + "preview": { + "label": "Recording Preview Config", + "properties": { + "quality": { + "label": "Quality of recording preview." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of recording." + } + } + }, + "review": { + "label": "Review configuration.", + "properties": { + "alerts": { + "label": "Review alerts config.", + "properties": { + "enabled": { + "label": "Enable alerts." + }, + "labels": { + "label": "Labels to create alerts for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as an alert." + }, + "enabled_in_config": { + "label": "Keep track of original state of alerts." + }, + "cutoff_time": { + "label": "Time to cutoff alerts after no alert-causing activity has occurred." + } + } + }, + "detections": { + "label": "Review detections config.", + "properties": { + "enabled": { + "label": "Enable detections." + }, + "labels": { + "label": "Labels to create detections for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as a detection." + }, + "cutoff_time": { + "label": "Time to cutoff detection after no detection-causing activity has occurred." + }, + "enabled_in_config": { + "label": "Keep track of original state of detections." + } + } + }, + "genai": { + "label": "Review description genai config.", + "properties": { + "enabled": { + "label": "Enable GenAI descriptions for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts." + }, + "detections": { + "label": "Enable GenAI for detections." + }, + "additional_concerns": { + "label": "Additional concerns that GenAI should make note of on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + }, + "preferred_language": { + "label": "Preferred language for GenAI Response" + }, + "activity_context_prompt": { + "label": "Custom activity context prompt defining normal activity patterns for this property." + } + } + } + } + }, + "semantic_search": { + "label": "Semantic search configuration.", + "properties": { + "triggers": { + "label": "Trigger actions on tracked objects that match existing thumbnails or descriptions", + "properties": { + "enabled": { + "label": "Enable this trigger" + }, + "type": { + "label": "Type of trigger" + }, + "data": { + "label": "Trigger content (text phrase or image ID)" + }, + "threshold": { + "label": "Confidence score required to run the trigger" + }, + "actions": { + "label": "Actions to perform when trigger is matched" + } + } + } + } + }, + "snapshots": { + "label": "Snapshot configuration.", + "properties": { + "enabled": { + "label": "Snapshots enabled." + }, + "clean_copy": { + "label": "Create a clean copy of the snapshot image." + }, + "timestamp": { + "label": "Add a timestamp overlay on the snapshot." + }, + "bounding_box": { + "label": "Add a bounding box overlay on the snapshot." + }, + "crop": { + "label": "Crop the snapshot to the detected object." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save a snapshot." + }, + "height": { + "label": "Snapshot image height." + }, + "retain": { + "label": "Snapshot retention.", + "properties": { + "default": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + }, + "objects": { + "label": "Object retention period." + } + } + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } + }, + "timestamp_style": { + "label": "Timestamp style configuration.", + "properties": { + "position": { + "label": "Timestamp position." + }, + "format": { + "label": "Timestamp format." + }, + "color": { + "label": "Timestamp color.", + "properties": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + }, + "blue": { + "label": "Blue" + } + } + }, + "thickness": { + "label": "Timestamp thickness." + }, + "effect": { + "label": "Timestamp effect." + } + } + }, + "best_image_timeout": { + "label": "How long to wait for the image with the highest confidence score." + }, + "mqtt": { + "label": "MQTT configuration.", + "properties": { + "enabled": { + "label": "Send image over MQTT." + }, + "timestamp": { + "label": "Add timestamp to MQTT image." + }, + "bounding_box": { + "label": "Add bounding box to MQTT image." + }, + "crop": { + "label": "Crop MQTT image to detected object." + }, + "height": { + "label": "MQTT image height." + }, + "required_zones": { + "label": "List of required zones to be entered in order to send the image." + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } + }, + "notifications": { + "label": "Notifications configuration.", + "properties": { + "enabled": { + "label": "Enable notifications" + }, + "email": { + "label": "Email required for push." + }, + "cooldown": { + "label": "Cooldown period for notifications (time in seconds)." + }, + "enabled_in_config": { + "label": "Keep track of original state of notifications." + } + } + }, + "onvif": { + "label": "Camera Onvif Configuration.", + "properties": { + "host": { + "label": "Onvif Host" + }, + "port": { + "label": "Onvif Port" + }, + "user": { + "label": "Onvif Username" + }, + "password": { + "label": "Onvif Password" + }, + "tls_insecure": { + "label": "Onvif Disable TLS verification" + }, + "autotracking": { + "label": "PTZ auto tracking config.", + "properties": { + "enabled": { + "label": "Enable PTZ object autotracking." + }, + "calibrate_on_startup": { + "label": "Perform a camera calibration when Frigate starts." + }, + "zooming": { + "label": "Autotracker zooming mode." + }, + "zoom_factor": { + "label": "Zooming factor (0.1-0.75)." + }, + "track": { + "label": "Objects to track." + }, + "required_zones": { + "label": "List of required zones to be entered in order to begin autotracking." + }, + "return_preset": { + "label": "Name of camera preset to return to when object tracking is over." + }, + "timeout": { + "label": "Seconds to delay before returning to preset." + }, + "movement_weights": { + "label": "Internal value used for PTZ movements based on the speed of your camera's motor." + }, + "enabled_in_config": { + "label": "Keep track of original state of autotracking." + } + } + }, + "ignore_time_mismatch": { + "label": "Onvif Ignore Time Synchronization Mismatch Between Camera and Server" + } + } + }, + "type": { + "label": "Camera Type" + }, + "ui": { + "label": "Camera UI Modifications.", + "properties": { + "order": { + "label": "Order of camera in UI." + }, + "dashboard": { + "label": "Show this camera in Frigate dashboard UI." + } + } + }, + "webui_url": { + "label": "URL to visit the camera directly from system page" + }, + "zones": { + "label": "Zone configuration.", + "properties": { + "filters": { + "label": "Zone filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "coordinates": { + "label": "Coordinates polygon for the defined zone." + }, + "distances": { + "label": "Real-world distances for the sides of quadrilateral for the defined zone." + }, + "inertia": { + "label": "Number of consecutive frames required for object to be considered present in the zone." + }, + "loitering_time": { + "label": "Number of seconds that an object must loiter to be considered in the zone." + }, + "speed_threshold": { + "label": "Minimum speed value for an object to be considered in the zone." + }, + "objects": { + "label": "List of objects that can trigger the zone." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/classification.json b/web/public/locales/en/config/classification.json new file mode 100644 index 000000000..e8014b2fa --- /dev/null +++ b/web/public/locales/en/config/classification.json @@ -0,0 +1,58 @@ +{ + "label": "Object classification config.", + "properties": { + "bird": { + "label": "Bird classification config.", + "properties": { + "enabled": { + "label": "Enable bird classification." + }, + "threshold": { + "label": "Minimum classification score required to be considered a match." + } + } + }, + "custom": { + "label": "Custom Classification Model Configs.", + "properties": { + "enabled": { + "label": "Enable running the model." + }, + "name": { + "label": "Name of classification model." + }, + "threshold": { + "label": "Classification score threshold to change the state." + }, + "object_config": { + "properties": { + "objects": { + "label": "Object types to classify." + }, + "classification_type": { + "label": "Type of classification that is applied." + } + } + }, + "state_config": { + "properties": { + "cameras": { + "label": "Cameras to run classification on.", + "properties": { + "crop": { + "label": "Crop of image frame on this camera to run classification on." + } + } + }, + "motion": { + "label": "If classification should be run when motion is detected in the crop." + }, + "interval": { + "label": "Interval to run classification on in seconds." + } + } + } + } + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/database.json b/web/public/locales/en/config/database.json new file mode 100644 index 000000000..ece7ccbaa --- /dev/null +++ b/web/public/locales/en/config/database.json @@ -0,0 +1,8 @@ +{ + "label": "Database configuration.", + "properties": { + "path": { + "label": "Database path." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/detect.json b/web/public/locales/en/config/detect.json new file mode 100644 index 000000000..9e1b59313 --- /dev/null +++ b/web/public/locales/en/config/detect.json @@ -0,0 +1,51 @@ +{ + "label": "Global object tracking configuration.", + "properties": { + "enabled": { + "label": "Detection Enabled." + }, + "height": { + "label": "Height of the stream for the detect role." + }, + "width": { + "label": "Width of the stream for the detect role." + }, + "fps": { + "label": "Number of frames per second to process through detection." + }, + "min_initialized": { + "label": "Minimum number of consecutive hits for an object to be initialized by the tracker." + }, + "max_disappeared": { + "label": "Maximum number of frames the object can disappear before detection ends." + }, + "stationary": { + "label": "Stationary objects config.", + "properties": { + "interval": { + "label": "Frame interval for checking stationary objects." + }, + "threshold": { + "label": "Number of frames without a position change for an object to be considered stationary" + }, + "max_frames": { + "label": "Max frames for stationary objects.", + "properties": { + "default": { + "label": "Default max frames." + }, + "objects": { + "label": "Object specific max frames." + } + } + }, + "classifier": { + "label": "Enable visual classifier for determing if objects with jittery bounding boxes are stationary." + } + } + }, + "annotation_offset": { + "label": "Milliseconds to offset detect annotations by." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/detectors.json b/web/public/locales/en/config/detectors.json new file mode 100644 index 000000000..1bd6fec70 --- /dev/null +++ b/web/public/locales/en/config/detectors.json @@ -0,0 +1,14 @@ +{ + "label": "Detector hardware configuration.", + "properties": { + "type": { + "label": "Detector Type" + }, + "model": { + "label": "Detector specific model configuration." + }, + "model_path": { + "label": "Detector specific model path." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/environment_vars.json b/web/public/locales/en/config/environment_vars.json new file mode 100644 index 000000000..ce97ce49e --- /dev/null +++ b/web/public/locales/en/config/environment_vars.json @@ -0,0 +1,3 @@ +{ + "label": "Frigate environment variables." +} \ No newline at end of file diff --git a/web/public/locales/en/config/face_recognition.json b/web/public/locales/en/config/face_recognition.json new file mode 100644 index 000000000..705d75468 --- /dev/null +++ b/web/public/locales/en/config/face_recognition.json @@ -0,0 +1,36 @@ +{ + "label": "Face recognition config.", + "properties": { + "enabled": { + "label": "Enable face recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "unknown_score": { + "label": "Minimum face distance score required to be marked as a potential match." + }, + "detection_threshold": { + "label": "Minimum face detection score required to be considered a face." + }, + "recognition_threshold": { + "label": "Minimum face distance score required to be considered a match." + }, + "min_area": { + "label": "Min area of face box to consider running face recognition." + }, + "min_faces": { + "label": "Min face recognitions for the sub label to be applied to the person object." + }, + "save_attempts": { + "label": "Number of face attempts to save in the recent recognitions tab." + }, + "blur_confidence_filter": { + "label": "Apply blur quality filter to face confidence." + }, + "device": { + "label": "The device key to use for face recognition.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/ffmpeg.json b/web/public/locales/en/config/ffmpeg.json new file mode 100644 index 000000000..570da5a35 --- /dev/null +++ b/web/public/locales/en/config/ffmpeg.json @@ -0,0 +1,34 @@ +{ + "label": "Global FFmpeg configuration.", + "properties": { + "path": { + "label": "FFmpeg path" + }, + "global_args": { + "label": "Global FFmpeg arguments." + }, + "hwaccel_args": { + "label": "FFmpeg hardware acceleration arguments." + }, + "input_args": { + "label": "FFmpeg input arguments." + }, + "output_args": { + "label": "FFmpeg output arguments per role.", + "properties": { + "detect": { + "label": "Detect role FFmpeg output arguments." + }, + "record": { + "label": "Record role FFmpeg output arguments." + } + } + }, + "retry_interval": { + "label": "Time in seconds to wait before FFmpeg retries connecting to the camera." + }, + "apple_compatibility": { + "label": "Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/genai.json b/web/public/locales/en/config/genai.json new file mode 100644 index 000000000..fed679d9e --- /dev/null +++ b/web/public/locales/en/config/genai.json @@ -0,0 +1,23 @@ +{ + "label": "Generative AI configuration.", + "properties": { + "api_key": { + "label": "Provider API key." + }, + "base_url": { + "label": "Provider base url." + }, + "model": { + "label": "GenAI model." + }, + "provider": { + "label": "GenAI provider." + }, + "provider_options": { + "label": "GenAI Provider extra options." + }, + "runtime_options": { + "label": "Options to pass during inference calls." + } + } +} diff --git a/web/public/locales/en/config/go2rtc.json b/web/public/locales/en/config/go2rtc.json new file mode 100644 index 000000000..76ec33020 --- /dev/null +++ b/web/public/locales/en/config/go2rtc.json @@ -0,0 +1,3 @@ +{ + "label": "Global restream configuration." +} \ No newline at end of file diff --git a/web/public/locales/en/config/live.json b/web/public/locales/en/config/live.json new file mode 100644 index 000000000..362170137 --- /dev/null +++ b/web/public/locales/en/config/live.json @@ -0,0 +1,14 @@ +{ + "label": "Live playback settings.", + "properties": { + "streams": { + "label": "Friendly names and restream names to use for live view." + }, + "height": { + "label": "Live camera view height" + }, + "quality": { + "label": "Live camera view quality" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/logger.json b/web/public/locales/en/config/logger.json new file mode 100644 index 000000000..3d51786a7 --- /dev/null +++ b/web/public/locales/en/config/logger.json @@ -0,0 +1,11 @@ +{ + "label": "Logging configuration.", + "properties": { + "default": { + "label": "Default logging level." + }, + "logs": { + "label": "Log level for specified processes." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/lpr.json b/web/public/locales/en/config/lpr.json new file mode 100644 index 000000000..951d1f8f6 --- /dev/null +++ b/web/public/locales/en/config/lpr.json @@ -0,0 +1,45 @@ +{ + "label": "License Plate recognition config.", + "properties": { + "enabled": { + "label": "Enable license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "detection_threshold": { + "label": "License plate object confidence score required to begin running recognition." + }, + "min_area": { + "label": "Minimum area of license plate to begin running recognition." + }, + "recognition_threshold": { + "label": "Recognition confidence score required to add the plate to the object as a sub label." + }, + "min_plate_length": { + "label": "Minimum number of characters a license plate must have to be added to the object as a sub label." + }, + "format": { + "label": "Regular expression for the expected format of license plate." + }, + "match_distance": { + "label": "Allow this number of missing/incorrect characters to still cause a detected plate to match a known plate." + }, + "known_plates": { + "label": "Known plates to track (strings or regular expressions)." + }, + "enhancement": { + "label": "Amount of contrast adjustment and denoising to apply to license plate images before recognition." + }, + "debug_save_plates": { + "label": "Save plates captured for LPR for debugging purposes." + }, + "device": { + "label": "The device key to use for LPR.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + }, + "replace_rules": { + "label": "List of regex replacement rules for normalizing detected plates. Each rule has 'pattern' and 'replacement'." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/model.json b/web/public/locales/en/config/model.json new file mode 100644 index 000000000..0bc2c1ddf --- /dev/null +++ b/web/public/locales/en/config/model.json @@ -0,0 +1,35 @@ +{ + "label": "Detection model configuration.", + "properties": { + "path": { + "label": "Custom Object detection model path." + }, + "labelmap_path": { + "label": "Label map for custom object detector." + }, + "width": { + "label": "Object detection model input width." + }, + "height": { + "label": "Object detection model input height." + }, + "labelmap": { + "label": "Labelmap customization." + }, + "attributes_map": { + "label": "Map of object labels to their attribute labels." + }, + "input_tensor": { + "label": "Model Input Tensor Shape" + }, + "input_pixel_format": { + "label": "Model Input Pixel Color Format" + }, + "input_dtype": { + "label": "Model Input D Type" + }, + "model_type": { + "label": "Object Detection Model Type" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/motion.json b/web/public/locales/en/config/motion.json new file mode 100644 index 000000000..183bfdf34 --- /dev/null +++ b/web/public/locales/en/config/motion.json @@ -0,0 +1,3 @@ +{ + "label": "Global motion detection configuration." +} \ No newline at end of file diff --git a/web/public/locales/en/config/mqtt.json b/web/public/locales/en/config/mqtt.json new file mode 100644 index 000000000..d2625ac83 --- /dev/null +++ b/web/public/locales/en/config/mqtt.json @@ -0,0 +1,44 @@ +{ + "label": "MQTT configuration.", + "properties": { + "enabled": { + "label": "Enable MQTT Communication." + }, + "host": { + "label": "MQTT Host" + }, + "port": { + "label": "MQTT Port" + }, + "topic_prefix": { + "label": "MQTT Topic Prefix" + }, + "client_id": { + "label": "MQTT Client ID" + }, + "stats_interval": { + "label": "MQTT Camera Stats Interval" + }, + "user": { + "label": "MQTT Username" + }, + "password": { + "label": "MQTT Password" + }, + "tls_ca_certs": { + "label": "MQTT TLS CA Certificates" + }, + "tls_client_cert": { + "label": "MQTT TLS Client Certificate" + }, + "tls_client_key": { + "label": "MQTT TLS Client Key" + }, + "tls_insecure": { + "label": "MQTT TLS Insecure" + }, + "qos": { + "label": "MQTT QoS" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/networking.json b/web/public/locales/en/config/networking.json new file mode 100644 index 000000000..0f8d9cc54 --- /dev/null +++ b/web/public/locales/en/config/networking.json @@ -0,0 +1,13 @@ +{ + "label": "Networking configuration", + "properties": { + "ipv6": { + "label": "Network configuration", + "properties": { + "enabled": { + "label": "Enable IPv6 for port 5000 and/or 8971" + } + } + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/notifications.json b/web/public/locales/en/config/notifications.json new file mode 100644 index 000000000..b529f10e0 --- /dev/null +++ b/web/public/locales/en/config/notifications.json @@ -0,0 +1,17 @@ +{ + "label": "Global notification configuration.", + "properties": { + "enabled": { + "label": "Enable notifications" + }, + "email": { + "label": "Email required for push." + }, + "cooldown": { + "label": "Cooldown period for notifications (time in seconds)." + }, + "enabled_in_config": { + "label": "Keep track of original state of notifications." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/objects.json b/web/public/locales/en/config/objects.json new file mode 100644 index 000000000..f041672a0 --- /dev/null +++ b/web/public/locales/en/config/objects.json @@ -0,0 +1,77 @@ +{ + "label": "Global object configuration.", + "properties": { + "track": { + "label": "Objects to track." + }, + "filters": { + "label": "Object filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "mask": { + "label": "Object mask." + }, + "genai": { + "label": "Config for using genai to analyze objects.", + "properties": { + "enabled": { + "label": "Enable GenAI for camera." + }, + "use_snapshot": { + "label": "Use snapshots for generating descriptions." + }, + "prompt": { + "label": "Default caption prompt." + }, + "object_prompts": { + "label": "Object specific prompts." + }, + "objects": { + "label": "List of objects to run generative AI for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to run generative AI." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "send_triggers": { + "label": "What triggers to use to send frames to generative AI for a tracked object.", + "properties": { + "tracked_object_end": { + "label": "Send once the object is no longer tracked." + }, + "after_significant_updates": { + "label": "Send an early request to generative AI when X frames accumulated." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + } + } + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/proxy.json b/web/public/locales/en/config/proxy.json new file mode 100644 index 000000000..732d6fafd --- /dev/null +++ b/web/public/locales/en/config/proxy.json @@ -0,0 +1,31 @@ +{ + "label": "Proxy configuration.", + "properties": { + "header_map": { + "label": "Header mapping definitions for proxy user passing.", + "properties": { + "user": { + "label": "Header name from upstream proxy to identify user." + }, + "role": { + "label": "Header name from upstream proxy to identify user role." + }, + "role_map": { + "label": "Mapping of Frigate roles to upstream group values. " + } + } + }, + "logout_url": { + "label": "Redirect url for logging out with proxy." + }, + "auth_secret": { + "label": "Secret value for proxy authentication." + }, + "default_role": { + "label": "Default role for proxy users." + }, + "separator": { + "label": "The character used to separate values in a mapped header." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/record.json b/web/public/locales/en/config/record.json new file mode 100644 index 000000000..81139084e --- /dev/null +++ b/web/public/locales/en/config/record.json @@ -0,0 +1,93 @@ +{ + "label": "Global record configuration.", + "properties": { + "enabled": { + "label": "Enable record on all cameras." + }, + "sync_recordings": { + "label": "Sync recordings with disk on startup and once a day." + }, + "expire_interval": { + "label": "Number of minutes to wait between cleanup runs." + }, + "continuous": { + "label": "Continuous recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "motion": { + "label": "Motion recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "detections": { + "label": "Detection specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "alerts": { + "label": "Alert specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "export": { + "label": "Recording Export Config", + "properties": { + "timelapse_args": { + "label": "Timelapse Args" + } + } + }, + "preview": { + "label": "Recording Preview Config", + "properties": { + "quality": { + "label": "Quality of recording preview." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of recording." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/review.json b/web/public/locales/en/config/review.json new file mode 100644 index 000000000..dba83ee1c --- /dev/null +++ b/web/public/locales/en/config/review.json @@ -0,0 +1,74 @@ +{ + "label": "Review configuration.", + "properties": { + "alerts": { + "label": "Review alerts config.", + "properties": { + "enabled": { + "label": "Enable alerts." + }, + "labels": { + "label": "Labels to create alerts for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as an alert." + }, + "enabled_in_config": { + "label": "Keep track of original state of alerts." + }, + "cutoff_time": { + "label": "Time to cutoff alerts after no alert-causing activity has occurred." + } + } + }, + "detections": { + "label": "Review detections config.", + "properties": { + "enabled": { + "label": "Enable detections." + }, + "labels": { + "label": "Labels to create detections for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as a detection." + }, + "cutoff_time": { + "label": "Time to cutoff detection after no detection-causing activity has occurred." + }, + "enabled_in_config": { + "label": "Keep track of original state of detections." + } + } + }, + "genai": { + "label": "Review description genai config.", + "properties": { + "enabled": { + "label": "Enable GenAI descriptions for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts." + }, + "detections": { + "label": "Enable GenAI for detections." + }, + "additional_concerns": { + "label": "Additional concerns that GenAI should make note of on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + }, + "preferred_language": { + "label": "Preferred language for GenAI Response" + }, + "activity_context_prompt": { + "label": "Custom activity context prompt defining normal activity patterns for this property." + } + } + } + } +} diff --git a/web/public/locales/en/config/safe_mode.json b/web/public/locales/en/config/safe_mode.json new file mode 100644 index 000000000..352f78b29 --- /dev/null +++ b/web/public/locales/en/config/safe_mode.json @@ -0,0 +1,3 @@ +{ + "label": "If Frigate should be started in safe mode." +} \ No newline at end of file diff --git a/web/public/locales/en/config/semantic_search.json b/web/public/locales/en/config/semantic_search.json new file mode 100644 index 000000000..2c46640bb --- /dev/null +++ b/web/public/locales/en/config/semantic_search.json @@ -0,0 +1,21 @@ +{ + "label": "Semantic search configuration.", + "properties": { + "enabled": { + "label": "Enable semantic search." + }, + "reindex": { + "label": "Reindex all tracked objects on startup." + }, + "model": { + "label": "The CLIP model to use for semantic search." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "device": { + "label": "The device key to use for semantic search.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/snapshots.json b/web/public/locales/en/config/snapshots.json new file mode 100644 index 000000000..a6336140e --- /dev/null +++ b/web/public/locales/en/config/snapshots.json @@ -0,0 +1,43 @@ +{ + "label": "Global snapshots configuration.", + "properties": { + "enabled": { + "label": "Snapshots enabled." + }, + "clean_copy": { + "label": "Create a clean copy of the snapshot image." + }, + "timestamp": { + "label": "Add a timestamp overlay on the snapshot." + }, + "bounding_box": { + "label": "Add a bounding box overlay on the snapshot." + }, + "crop": { + "label": "Crop the snapshot to the detected object." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save a snapshot." + }, + "height": { + "label": "Snapshot image height." + }, + "retain": { + "label": "Snapshot retention.", + "properties": { + "default": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + }, + "objects": { + "label": "Object retention period." + } + } + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/telemetry.json b/web/public/locales/en/config/telemetry.json new file mode 100644 index 000000000..802ced2a0 --- /dev/null +++ b/web/public/locales/en/config/telemetry.json @@ -0,0 +1,28 @@ +{ + "label": "Telemetry configuration.", + "properties": { + "network_interfaces": { + "label": "Enabled network interfaces for bandwidth calculation." + }, + "stats": { + "label": "System Stats Configuration", + "properties": { + "amd_gpu_stats": { + "label": "Enable AMD GPU stats." + }, + "intel_gpu_stats": { + "label": "Enable Intel GPU stats." + }, + "network_bandwidth": { + "label": "Enable network bandwidth for ffmpeg processes." + }, + "intel_gpu_device": { + "label": "Define the device to use when gathering SR-IOV stats." + } + } + }, + "version_check": { + "label": "Enable latest version check." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/timestamp_style.json b/web/public/locales/en/config/timestamp_style.json new file mode 100644 index 000000000..6a3119423 --- /dev/null +++ b/web/public/locales/en/config/timestamp_style.json @@ -0,0 +1,31 @@ +{ + "label": "Global timestamp style configuration.", + "properties": { + "position": { + "label": "Timestamp position." + }, + "format": { + "label": "Timestamp format." + }, + "color": { + "label": "Timestamp color.", + "properties": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + }, + "blue": { + "label": "Blue" + } + } + }, + "thickness": { + "label": "Timestamp thickness." + }, + "effect": { + "label": "Timestamp effect." + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/tls.json b/web/public/locales/en/config/tls.json new file mode 100644 index 000000000..58493ff40 --- /dev/null +++ b/web/public/locales/en/config/tls.json @@ -0,0 +1,8 @@ +{ + "label": "TLS configuration.", + "properties": { + "enabled": { + "label": "Enable TLS for port 8971" + } + } +} \ No newline at end of file diff --git a/web/public/locales/en/config/ui.json b/web/public/locales/en/config/ui.json new file mode 100644 index 000000000..cdd91cb53 --- /dev/null +++ b/web/public/locales/en/config/ui.json @@ -0,0 +1,20 @@ +{ + "label": "UI configuration.", + "properties": { + "timezone": { + "label": "Override UI timezone." + }, + "time_format": { + "label": "Override UI time format." + }, + "date_style": { + "label": "Override UI dateStyle." + }, + "time_style": { + "label": "Override UI timeStyle." + }, + "unit_system": { + "label": "The unit system to use for measurements." + } + } +} diff --git a/web/public/locales/en/config/version.json b/web/public/locales/en/config/version.json new file mode 100644 index 000000000..e777d7573 --- /dev/null +++ b/web/public/locales/en/config/version.json @@ -0,0 +1,3 @@ +{ + "label": "Current config version." +} \ No newline at end of file diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json new file mode 100644 index 000000000..a07114b5c --- /dev/null +++ b/web/public/locales/en/views/classificationModel.json @@ -0,0 +1,187 @@ +{ + "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": { + "deletedCategory": "Deleted Class", + "deletedImage": "Deleted Images", + "deletedModel_one": "Successfully deleted {{count}} model", + "deletedModel_other": "Successfully deleted {{count}} models", + "categorizedImage": "Successfully Classified Image", + "trainedModel": "Successfully trained model.", + "trainingModel": "Successfully started model training.", + "updatedModel": "Successfully updated model configuration", + "renamedCategory": "Successfully renamed class to {{name}}" + }, + "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}}" + } + }, + "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", + "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", + "missingStatesWarning": { + "title": "Missing State Examples", + "description": "It's recommended to select examples for all states for best results. You can continue without selecting all states, but the model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model." + } + } + } +} diff --git a/web/public/locales/en/views/configEditor.json b/web/public/locales/en/views/configEditor.json index ef3035f38..614143c16 100644 --- a/web/public/locales/en/views/configEditor.json +++ b/web/public/locales/en/views/configEditor.json @@ -1,6 +1,8 @@ { "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", diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index 98bc7c422..ea3ee853d 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -9,15 +9,38 @@ "empty": { "alert": "There are no alerts to review", "detection": "There are no detections to review", - "motion": "No motion data found" + "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": "Timeline", "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" @@ -33,6 +56,10 @@ }, "selected_one": "{{count}} selected", "selected_other": "{{count}} selected", + "select_all": "All", "camera": "Camera", - "detected": "detected" + "detected": "detected", + "normalActivity": "Normal", + "needsReview": "Needs review", + "securityConcern": "Security concern" } diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 7e2381445..53b04e6c4 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -24,8 +24,7 @@ "textTokenizer": "Text tokenizer" }, "tips": { - "context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded.", - "documentation": "Read the documentation" + "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." } @@ -34,15 +33,16 @@ "type": { "details": "details", "snapshot": "snapshot", + "thumbnail": "thumbnail", "video": "video", - "object_lifecycle": "object lifecycle" + "tracking_details": "tracking details" }, - "objectLifecycle": { - "title": "Object Lifecycle", + "trackingDetails": { + "title": "Tracking Details", "noImageFound": "No image found for this timestamp.", "createObjectMask": "Create Object Mask", "adjustAnnotationSettings": "Adjust annotation settings", - "scrollViewTips": "Scroll to view the significant moments of this object's lifecycle.", + "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", @@ -61,7 +61,8 @@ "header": { "zones": "Zones", "ratio": "Ratio", - "area": "Area" + "area": "Area", + "score": "Score" } }, "annotationSettings": { @@ -72,12 +73,11 @@ }, "offset": { "label": "Annotation Offset", - "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. However, the annotation_offset field can be used to adjust this.", - "documentation": "Read the documentation ", + "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", "millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0", - "tips": "TIP: Imagine there is an event clip with a person walking from left to right. If the event timeline 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.", + "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. Restart Frigate to apply your changes." + "success": "Annotation offset for {{camera}} has been saved to the config file." } } }, @@ -103,12 +103,16 @@ "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." + "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}}" + "updatedLPRFailed": "Failed to update license plate: {{errorMessage}}", + "updatedAttributesFailed": "Failed to update attributes: {{errorMessage}}", + "audioTranscription": "Failed to request audio transcription: {{errorMessage}}" } } }, @@ -123,6 +127,10 @@ "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" }, @@ -130,7 +138,11 @@ "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", @@ -154,6 +166,9 @@ "tips": { "descriptionSaved": "Successfully saved description", "saveDescriptionFailed": "Failed to update the description: {{errorMessage}}" + }, + "title": { + "label": "Title" } }, "itemMenu": { @@ -165,14 +180,26 @@ "label": "Download snapshot", "aria": "Download snapshot" }, - "viewObjectLifecycle": { - "label": "View object lifecycle", - "aria": "Show the object lifecycle" + "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" @@ -183,12 +210,18 @@ }, "deleteTrackedObject": { "label": "Delete this tracked object" + }, + "showObjectDetails": { + "label": "Show object path" + }, + "hideObjectDetails": { + "label": "Hide object path" } }, "dialog": { "confirmDelete": { "title": "Confirm Delete", - "desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of this tracked object in History view will NOT be deleted.

    Are you sure you want to proceed?" + "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?" } }, "noTrackedObjects": "No Tracked Objects Found", @@ -197,11 +230,19 @@ "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" } } diff --git a/web/public/locales/en/views/exports.json b/web/public/locales/en/views/exports.json index 729899443..4a79d20e1 100644 --- a/web/public/locales/en/views/exports.json +++ b/web/public/locales/en/views/exports.json @@ -9,6 +9,12 @@ "desc": "Enter a new name for this export.", "saveExport": "Save Export" }, + "tooltip": { + "shareExport": "Share export", + "downloadVideo": "Download video", + "editName": "Edit name", + "deleteExport": "Delete export" + }, "toast": { "error": { "renameExportFailed": "Failed to rename export: {{errorMessage}}" diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index e734ca974..2dbb1a4fd 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -1,17 +1,13 @@ { "description": { - "addFace": "Walk through adding a new collection to the Face Library.", + "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." }, "details": { - "person": "Person", - "subLabelScore": "Sub Label Score", - "scoreInfo": "The sub label score is the weighted score for all of the recognized face confidences, so this may differ from the score shown on the snapshot.", - "face": "Face Details", - "faceDesc": "Details of the tracked object that generated this face", "timestamp": "Timestamp", - "unknown": "Unknown" + "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": { @@ -20,10 +16,8 @@ }, "collections": "Collections", "createFaceLibrary": { - "title": "Create Collection", - "desc": "Create a new collection", "new": "Create New Face", - "nextSteps": "To build a strong foundation:
  • Use the Train 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.
  • " + "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", @@ -34,12 +28,11 @@ } }, "train": { - "title": "Train", - "aria": "Select train", + "title": "Recent Recognitions", + "titleShort": "Recent", + "aria": "Select recent recognitions", "empty": "There are no recent face recognition attempts" }, - "selectItem": "Select {{item}}", - "selectFace": "Select Face", "deleteFaceLibrary": { "title": "Delete Name", "desc": "Are you sure you want to delete the collection {{name}}? This will permanently delete all associated faces." @@ -66,12 +59,10 @@ "selectImage": "Please select an image file." }, "dropActive": "Drop the image here…", - "dropInstructions": "Drag and drop an image here, or click to select", + "dropInstructions": "Drag and drop or paste an image here, or click to select", "maxSize": "Max size: {{size}}MB" }, "nofaces": "No faces available", - "pixels": "{{area}}px", - "readTheDocs": "Read the documentation", "trainFaceAs": "Train Face as:", "trainFace": "Train Face", "toast": { @@ -85,7 +76,7 @@ "deletedName_other": "{{count}} faces have been successfully deleted.", "renamedFace": "Successfully renamed face to {{name}}", "trainedFace": "Successfully trained face.", - "updatedFaceScore": "Successfully updated face score." + "updatedFaceScore": "Successfully updated face score to {{name}} ({{score}})." }, "error": { "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index 1790467d2..c2efef84f 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -38,6 +38,14 @@ "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" @@ -65,10 +73,20 @@ "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" @@ -78,8 +96,8 @@ "disable": "Hide Stream Stats" }, "manualRecording": { - "title": "On-Demand Recording", - "tips": "Start a manual event based on this camera's recording retention settings.", + "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." @@ -107,15 +125,16 @@ "title": "Stream", "audio": { "tips": { - "title": "Audio must be output from your camera and configured in go2rtc for this stream.", - "documentation": "Read the documentation " + "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.", - "tips.documentation": "Read the documentation ", "available": "Two-way talk is available for this stream", "unavailable": "Two-way talk is unavailable for this stream" }, @@ -135,6 +154,7 @@ "recording": "Recording", "snapshots": "Snapshots", "audioDetection": "Audio Detection", + "transcription": "Audio Transcription", "autotracking": "Autotracking" }, "history": { @@ -145,8 +165,7 @@ "all": "All", "motion": "Motion", "active_objects": "Active Objects" - }, - "notAllTips": "Your {{source}} recording retention configuration is set to mode: {{effectiveRetainMode}}, so this on-demand recording will only keep segments with {{effectiveRetainModeName}}." + } }, "editLayout": { "label": "Edit Layout", @@ -154,5 +173,24 @@ "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/search.json b/web/public/locales/en/views/search.json index 22da7721f..dae622c70 100644 --- a/web/public/locales/en/views/search.json +++ b/web/public/locales/en/views/search.json @@ -16,6 +16,7 @@ "labels": "Labels", "zones": "Zones", "sub_labels": "Sub Labels", + "attributes": "Attributes", "search_type": "Search Type", "time_range": "Time Range", "before": "Before", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 2b92e81cd..9f211a442 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -2,23 +2,27 @@ "documentTitle": { "default": "Settings - Frigate", "authentication": "Authentication Settings - Frigate", - "camera": "Camera 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": "General Settings - Frigate", + "general": "UI Settings - Frigate", "frigatePlus": "Frigate+ Settings - Frigate", "notifications": "Notification Settings - Frigate" }, "menu": { "ui": "UI", "enrichments": "Enrichments", - "cameras": "Camera Settings", + "cameraManagement": "Management", + "cameraReview": "Review", "masksAndZones": "Masks / Zones", "motionTuner": "Motion Tuner", + "triggers": "Triggers", "debug": "Debug", "users": "Users", + "roles": "Roles", "notifications": "Notifications", "frigateplus": "Frigate+" }, @@ -33,7 +37,7 @@ "noCamera": "No Camera" }, "general": { - "title": "General Settings", + "title": "UI Settings", "liveDashboard": { "title": "Live Dashboard", "automaticLiveView": { @@ -43,6 +47,14 @@ "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": { @@ -92,7 +104,6 @@ "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.", - "readTheDocumentation": "Read the Documentation", "reindexNow": { "label": "Reindex Now", "desc": "Reindexing will regenerate embeddings for all tracked object. 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.", @@ -119,7 +130,6 @@ "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.", - "readTheDocumentation": "Read the Documentation", "modelSize": { "label": "Model Size", "desc": "The size of the model used for face recognition.", @@ -135,8 +145,7 @@ }, "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.", - "readTheDocumentation": "Read the Documentation" + "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": { @@ -144,12 +153,245 @@ "error": "Failed to save config changes: {{errorMessage}}" } }, - "camera": { - "title": "Camera Settings", + "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://\". 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.", + "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", + "addCamera": "Add New Camera", + "editCamera": "Edit Camera:", + "selectCamera": "Select a Camera", + "backToSettings": "Back to Camera Settings", "streams": { - "title": "Streams", + "title": "Enable / Disable Cameras", "desc": "Temporarily disable a camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
    Note: This does not disable go2rtc restreams." }, + "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" + } + } + }, + "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. ", @@ -159,7 +401,7 @@ "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.", - "readTheDocumentation": "Read the Documentation", + "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.", @@ -200,7 +442,8 @@ "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." + "hasIllegalCharacter": "Zone name contains illegal characters.", + "mustHaveAtLeastOneLetter": "Zone name must have at least one letter." } }, "distance": { @@ -258,7 +501,7 @@ "name": { "title": "Name", "inputPlaceHolder": "Enter a name…", - "tips": "Name must be at least 2 characters and must not be the name of a camera or another zone." + "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." }, "inertia": { "title": "Inertia", @@ -276,7 +519,6 @@ "speedEstimation": { "title": "Speed Estimation", "desc": "Enable speed estimation for objects in this zone. The zone must have exactly 4 points.", - "docs": "Read the documentation", "lineADistance": "Line A distance ({{unit}})", "lineBDistance": "Line B distance ({{unit}})", "lineCDistance": "Line C distance ({{unit}})", @@ -293,7 +535,7 @@ } }, "toast": { - "success": "Zone ({{zoneName}}) has been saved. Restart Frigate to apply changes." + "success": "Zone ({{zoneName}}) has been saved." } }, "motionMasks": { @@ -306,21 +548,19 @@ "add": "New Motion Mask", "edit": "Edit Motion Mask", "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.", - "documentation": "Read the documentation" + "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.", "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.", - "documentation": "Read the documentation" + "tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead." }, "toast": { "success": { - "title": "{{polygonName}} has been saved. Restart Frigate to apply changes.", - "noName": "Motion Mask has been saved. Restart Frigate to apply changes." + "title": "{{polygonName}} has been saved.", + "noName": "Motion Mask has been saved." } } }, @@ -344,8 +584,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} has been saved. Restart Frigate to apply changes.", - "noName": "Object Mask has been saved. Restart Frigate to apply changes." + "title": "{{polygonName}} has been saved.", + "noName": "Object Mask has been saved." } } } @@ -377,9 +617,17 @@ "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", @@ -410,11 +658,15 @@ "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.", - "document": "Read the documentation ", "score": "Score", "ratio": "Ratio", "area": "Area" @@ -427,7 +679,7 @@ "desc": "Manage this Frigate instance's user accounts." }, "addUser": "Add User", - "updatePassword": "Update Password", + "updatePassword": "Reset Password", "toast": { "success": { "createUser": "User {{user}} created successfully", @@ -448,7 +700,7 @@ "role": "Role", "noUsers": "No users found.", "changeRole": "Change user role", - "password": "Password", + "password": "Reset Password", "deleteUser": "Delete user" }, "dialog": { @@ -461,6 +713,8 @@ "password": { "title": "Password", "placeholder": "Enter password", + "show": "Show password", + "hide": "Hide password", "confirm": { "title": "Confirm Password", "placeholder": "Confirm Password" @@ -472,6 +726,13 @@ "strong": "Strong", "veryStrong": "Very Strong" }, + "requirements": { + "title": "Password requirements:", + "length": "At least 8 characters", + "uppercase": "At least one uppercase letter", + "digit": "At least one digit", + "special": "At least one special character (!@#$%^&*(),.?\":{}|<>)" + }, "match": "Passwords match", "notMatch": "Passwords don't match" }, @@ -482,6 +743,10 @@ "placeholder": "Re-enter new password" } }, + "currentPassword": { + "title": "Current Password", + "placeholder": "Enter your current password" + }, "usernameIsRequired": "Username is required", "passwordIsRequired": "Password is required" }, @@ -499,9 +764,14 @@ "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." + "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", @@ -512,7 +782,69 @@ "admin": "Admin", "adminDesc": "Full access to all features.", "viewer": "Viewer", - "viewerDesc": "Limited to Live dashboards, Review, Explore, and Exports only." + "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." } } } @@ -521,13 +853,11 @@ "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.", - "documentation": "Read the Documentation" + "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.", - "documentation": "Read the Documentation" + "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications." }, "globalSettings": { "title": "Global Settings", @@ -584,7 +914,6 @@ "snapshotConfig": { "title": "Snapshot Configuration", "desc": "Submitting to Frigate+ requires both snapshots and clean_copy snapshots to be enabled in your config.", - "documentation": "Read the documentation", "cleanCopyWarning": "Some cameras have snapshots enabled but have the clean copy disabled. You need to enable clean_copy in your snapshot config to be able to submit images from these cameras to Frigate+.", "table": { "camera": "Camera", @@ -615,5 +944,126 @@ "success": "Frigate+ settings have been saved. Restart Frigate to apply changes.", "error": "Failed to save config changes: {{errorMessage}}" } + }, + "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}}" + } + } } } diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index 059f05f9f..da774e302 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -42,6 +42,7 @@ "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": { @@ -75,12 +76,24 @@ } }, "npuUsage": "NPU Usage", - "npuMemory": "NPU Memory" + "npuMemory": "NPU Memory", + "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" + "processMemoryUsage": "Process Memory Usage", + "series": { + "go2rtc": "go2rtc", + "recording": "recording", + "review_segment": "review segment", + "embeddings": "embeddings", + "audio_detector": "audio detector" + } } }, "storage": { @@ -91,6 +104,10 @@ "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." + }, "cameraStorage": { "title": "Camera Storage", "camera": "Camera", @@ -158,11 +175,13 @@ "reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)", "cameraIsOffline": "{{camera}} is offline", "detectIsSlow": "{{detect}} is slow ({{speed}} ms)", - "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)" + "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)", + "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB." }, "enrichments": { "title": "Enrichments", "infPerSecond": "Inferences Per Second", + "averageInf": "Average Inference Time", "embeddings": { "image_embedding": "Image Embedding", "text_embedding": "Text Embedding", @@ -174,7 +193,16 @@ "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" + "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 index 16288b261..2641cb561 100644 --- a/web/public/locales/es/audio.json +++ b/web/public/locales/es/audio.json @@ -31,7 +31,7 @@ "crying": "Llanto", "synthetic_singing": "Canto sintético", "rapping": "Rap", - "humming": "Tarareo", + "humming": "Zumbido leve", "groan": "Gemido", "grunt": "Gruñido", "whistling": "Silbido", @@ -129,7 +129,7 @@ "sitar": "Sitar", "mandolin": "Mandolina", "zither": "Cítara", - "ukulele": "Ukulele", + "ukulele": "Ukelele", "piano": "Piano", "organ": "Órgano", "electronic_organ": "Órgano electrónico", @@ -153,7 +153,7 @@ "mallet_percussion": "Percusión con mazas", "marimba": "Marimba", "glockenspiel": "Glockenspiel", - "steelpan": "Steelpan", + "steelpan": "SarténAcero", "orchestra": "Orquesta", "trumpet": "Trompeta", "string_section": "Sección de cuerdas", @@ -183,13 +183,13 @@ "psychedelic_rock": "Rock psicodélico", "rhythm_and_blues": "Rhythm and blues", "soul_music": "Música soul", - "country": "Country", + "country": "País", "swing_music": "Música swing", "disco": "Disco", "house_music": "Música House", "dubstep": "Dubstep", "drum_and_bass": "Drum and Bass", - "electronica": "Electronica", + "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", @@ -207,7 +207,7 @@ "song": "Canción", "background_music": "Música Background", "soundtrack_music": "Música de Pelicula", - "lullaby": "Lullaby", + "lullaby": "Cancion de cuna", "video_game_music": "Música de Videojuego", "christmas_music": "Música Navideña", "sad_music": "Música triste", @@ -425,5 +425,79 @@ "radio": "Radio", "gunshot": "Disparo", "fusillade": "Descarga de Fusilería", - "pink_noise": "Ruido Rosa" + "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 index bf6a735fa..13d094ac2 100644 --- a/web/public/locales/es/common.json +++ b/web/public/locales/es/common.json @@ -87,7 +87,10 @@ "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" }, "menu": { "settings": "Ajustes", @@ -141,7 +144,15 @@ "fr": "Français (Frances)", "yue": "粵語 (Cantonés)", "th": "ไทย (Tailandés)", - "ca": "Català (Catalan)" + "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)" }, "appearance": "Apariencia", "darkMode": { @@ -181,7 +192,8 @@ "review": "Revisar", "explore": "Explorar", "uiPlayground": "Zona de pruebas de la interfaz de usuario", - "faceLibrary": "Biblioteca de rostros" + "faceLibrary": "Biblioteca de rostros", + "classification": "Clasificación" }, "unit": { "speed": { @@ -191,6 +203,14 @@ "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": { @@ -228,7 +248,8 @@ "enabled": "Habilitado", "saving": "Guardando…", "exitFullscreen": "Salir de pantalla completa", - "on": "ENCENDIDO" + "on": "ENCENDIDO", + "continue": "Continuar" }, "toast": { "save": { @@ -241,7 +262,12 @@ "copyUrlToClipboard": "URL copiada al portapapeles." }, "label": { - "back": "Volver atrás" + "back": "Volver atrás", + "hide": "Ocultar {{item}}", + "show": "Mostrar {{item}}", + "ID": "ID", + "none": "Ninguno", + "all": "Todas" }, "role": { "title": "Rol", @@ -271,5 +297,18 @@ "title": "404", "desc": "Página no encontrada" }, - "selectItem": "Seleccionar {{item}}" + "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" + } } diff --git a/web/public/locales/es/components/auth.json b/web/public/locales/es/components/auth.json index fde9c5a9f..62d6c8445 100644 --- a/web/public/locales/es/components/auth.json +++ b/web/public/locales/es/components/auth.json @@ -10,6 +10,7 @@ "loginFailed": "Error de inicio de sesión" }, "password": "Contraseña", - "login": "Iniciar sesión" + "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 index bf036e0ae..69605875e 100644 --- a/web/public/locales/es/components/camera.json +++ b/web/public/locales/es/components/camera.json @@ -66,7 +66,8 @@ "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" } }, "debug": { diff --git a/web/public/locales/es/components/dialog.json b/web/public/locales/es/components/dialog.json index 376b385e6..98c96528f 100644 --- a/web/public/locales/es/components/dialog.json +++ b/web/public/locales/es/components/dialog.json @@ -66,10 +66,11 @@ "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." + "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 carpeta /exports." + "success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.", + "view": "Ver" }, "fromTimeline": { "saveExport": "Guardar exportación", @@ -120,7 +121,16 @@ "button": { "export": "Exportar", "markAsReviewed": "Marcar como revisado", - "deleteNow": "Eliminar ahora" + "deleteNow": "Eliminar ahora", + "markAsUnreviewed": "Marcar como no revisado" } + }, + "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 index 7c627ad5f..d9d77d6f9 100644 --- a/web/public/locales/es/components/filter.json +++ b/web/public/locales/es/components/filter.json @@ -119,9 +119,23 @@ "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." + "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": "Clasificación de Atributos", + "all": "Todos los Atributos" } } diff --git a/web/public/locales/es/objects.json b/web/public/locales/es/objects.json index 0e972102c..0fd02208a 100644 --- a/web/public/locales/es/objects.json +++ b/web/public/locales/es/objects.json @@ -102,7 +102,7 @@ "baseball_bat": "Bate de béisbol", "oven": "Horno", "waste_bin": "Papelera", - "snowboard": "Snowboard", + "snowboard": "Tabla de Snow", "sandwich": "Sandwich", "fox": "Zorro", "nzpost": "NZPost", diff --git a/web/public/locales/es/views/classificationModel.json b/web/public/locales/es/views/classificationModel.json new file mode 100644 index 000000000..8d6087a8d --- /dev/null +++ b/web/public/locales/es/views/classificationModel.json @@ -0,0 +1,192 @@ +{ + "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": "Clase Borrada", + "deletedImage": "Imágenes Borradas", + "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}}" + }, + "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": "Falló el renombrado de la clase: {{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 media de clasificación en todas las detecciones de este objeto.", + "unknown": "Desconocido", + "none": "Nada" + }, + "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": "Se recomienda seleccionar ejemplos para todos los estados para obtener mejores resultados. Puede continuar sin seleccionar todos los estados, pero el modelo no se entrenará hasta que todos los estados tengan imágenes. Después de continuar, use la vista \"Clasificaciones recientes\" para clasificar las imágenes de los estados faltantes y luego entrene el modelo." + }, + "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." + }, + "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" + } + } +} diff --git a/web/public/locales/es/views/configEditor.json b/web/public/locales/es/views/configEditor.json index 39514ec82..3b9f2779e 100644 --- a/web/public/locales/es/views/configEditor.json +++ b/web/public/locales/es/views/configEditor.json @@ -12,5 +12,7 @@ } }, "documentTitle": "Editor de Configuración - Frigate", - "confirm": "¿Salir sin guardar?" + "confirm": "¿Salir sin guardar?", + "safeConfigEditor": "Editor de Configuración (Modo Seguro)", + "safeModeDescription": "Frigate esta en modo seguro debido a un error en la configuración." } diff --git a/web/public/locales/es/views/events.json b/web/public/locales/es/views/events.json index b06cd92e9..d13daff60 100644 --- a/web/public/locales/es/views/events.json +++ b/web/public/locales/es/views/events.json @@ -9,7 +9,11 @@ "empty": { "alert": "No hay alertas para revisar", "detection": "No hay detecciones para revisar", - "motion": "No se encontraron datos de movimiento" + "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": "Línea de tiempo", "timeline.aria": "Seleccionar línea de tiempo", @@ -35,5 +39,30 @@ "selected": "{{count}} seleccionados", "selected_one": "{{count}} seleccionados", "selected_other": "{{count}} seleccionados", - "detected": "detectado" + "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" } diff --git a/web/public/locales/es/views/explore.json b/web/public/locales/es/views/explore.json index f5fb869e0..f8f61ce83 100644 --- a/web/public/locales/es/views/explore.json +++ b/web/public/locales/es/views/explore.json @@ -41,12 +41,16 @@ "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." + "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}}" + "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": { @@ -97,6 +101,17 @@ "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" } }, "documentTitle": "Explorar - Frigate", @@ -105,7 +120,9 @@ "snapshot": "captura instantánea", "video": "vídeo", "object_lifecycle": "ciclo de vida del objeto", - "details": "detalles" + "details": "detalles", + "thumbnail": "miniatura", + "tracking_details": "detalles de seguimiento" }, "objectLifecycle": { "title": "Ciclo de vida del objeto", @@ -183,12 +200,34 @@ }, "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 el objeto seguido" + }, + "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" } }, "dialog": { "confirmDelete": { "title": "Confirmar eliminación", - "desc": "Eliminar este objeto rastreado elimina la captura de pantalla, cualquier incrustación guardada y cualquier entrada asociada al ciclo de vida del objeto. Las grabaciones de este objeto rastreado en la vista de Historial NO se eliminarán.

    ¿Estás seguro de que quieres proceder?" + "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?" } }, "noTrackedObjects": "No se encontraron objetos rastreados", @@ -200,10 +239,67 @@ "error": "No se pudo eliminar el objeto rastreado: {{errorMessage}}" } }, - "tooltip": "Coincidencia con {{type}} al {{confidence}}%" + "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}}" + "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}} ha sido activado/a", + "stationary": "{{label}} se volvió estacionaria", + "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)" + }, + "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 index b3b686cae..9de2fa330 100644 --- a/web/public/locales/es/views/exports.json +++ b/web/public/locales/es/views/exports.json @@ -13,5 +13,11 @@ "renameExportFailed": "No se pudo renombrar la exportación: {{errorMessage}}" } }, - "deleteExport.desc": "¿Estás seguro de que quieres eliminar {{exportName}}?" + "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" + } } diff --git a/web/public/locales/es/views/faceLibrary.json b/web/public/locales/es/views/faceLibrary.json index 5fe5baec4..44e1eba01 100644 --- a/web/public/locales/es/views/faceLibrary.json +++ b/web/public/locales/es/views/faceLibrary.json @@ -1,8 +1,8 @@ { "description": { - "addFace": "Guía para agregar una nueva colección a la Biblioteca de Rostros.", + "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 inválido. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos y guiones." + "invalidName": "Nombre incorrecto. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos, y guiones." }, "details": { "person": "Persona", @@ -23,12 +23,13 @@ "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 Entrenar para seleccionar y entrenar con imágenes de cada persona detectada.
  • Enfócate en imágenes frontales para obtener los mejores resultados; evita entrenar con imágenes que capturen rostros en ángulo.
  • " + "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": "Entrenar", - "aria": "Seleccionar entrenamiento", - "empty": "No hay intentos recientes de reconocimiento facial" + "title": "Reconocimientos Recientes", + "aria": "Seleccionar reconocimientos recientes", + "empty": "No hay intentos recientes de reconocimiento facial", + "titleShort": "Reciente" }, "selectItem": "Seleccionar {{item}}", "selectFace": "Seleccionar rostro", @@ -49,7 +50,7 @@ "selectImage": "Por favor, selecciona un archivo de imagen." }, "dropActive": "Suelta la imagen aquí…", - "dropInstructions": "Arrastra y suelta una imagen aquí, o haz clic para seleccionar", + "dropInstructions": "Arrastra y suelta, o pega una imagen aquí, o haz clic para seleccionar", "maxSize": "Tamaño máximo: {{size}}MB" }, "toast": { @@ -59,10 +60,10 @@ "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.", - "deletedFace_one": "{{count}} rostro eliminado con éxito", - "deletedFace_many": "{{count}} rostros eliminados con éxito", - "deletedFace_other": "{{count}} rostros 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}}" }, diff --git a/web/public/locales/es/views/live.json b/web/public/locales/es/views/live.json index 8191aebb8..664f7abec 100644 --- a/web/public/locales/es/views/live.json +++ b/web/public/locales/es/views/live.json @@ -42,7 +42,15 @@ "label": "Haz clic en el marco para centrar la cámara PTZ" } }, - "presets": "Preajustes de 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", @@ -77,8 +85,8 @@ "disable": "Ocultar estadísticas de transmisión" }, "manualRecording": { - "title": "Grabación bajo demanda", - "tips": "Iniciar un evento manual basado en la configuración de retención de grabaciones de esta cámara.", + "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." @@ -116,7 +124,7 @@ "twoWayTalk": { "tips.documentation": "Leer la documentación ", "available": "La conversación bidireccional está disponible para esta transmisión", - "unavailable": "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": { @@ -126,6 +134,9 @@ "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": { @@ -135,7 +146,8 @@ "recording": "Grabación", "snapshots": "Capturas de pantalla", "autotracking": "Seguimiento automático", - "cameraEnabled": "Cámara habilitada" + "cameraEnabled": "Cámara habilitada", + "transcription": "Transcripción de Audio" }, "history": { "label": "Mostrar grabaciones históricas" @@ -154,5 +166,24 @@ "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." + } + }, + "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/search.json b/web/public/locales/es/views/search.json index 7458c491d..547b17f4e 100644 --- a/web/public/locales/es/views/search.json +++ b/web/public/locales/es/views/search.json @@ -26,7 +26,8 @@ "max_speed": "Velocidad Máxima", "recognized_license_plate": "Matrícula Reconocida", "has_clip": "Tiene Clip", - "has_snapshot": "Tiene Instantánea" + "has_snapshot": "Tiene Instantánea", + "attributes": "Atributos" }, "searchType": { "thumbnail": "Miniatura", diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json index 6950c7999..e9745c4f7 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -7,10 +7,12 @@ "camera": "Configuración de cámara - Frigate", "motionTuner": "Ajuste de movimiento - Frigate", "classification": "Configuración de clasificación - Frigate", - "general": "Configuración General - Frigate", + "general": "Configuración de Interfaz de Usuario - Frigate", "frigatePlus": "Configuración de Frigate+ - Frigate", "notifications": "Configuración de Notificaciones - Frigate", - "enrichments": "Configuración de Análisis Avanzado - Frigate" + "enrichments": "Configuración de Análisis Avanzado - Frigate", + "cameraManagement": "Administrar Cámaras - Frigate", + "cameraReview": "Revisar Configuración de Cámaras - Frigate" }, "menu": { "cameras": "Configuración de Cámara", @@ -22,7 +24,11 @@ "frigateplus": "Frigate+", "users": "Usuarios", "notifications": "Notificaciones", - "enrichments": "Análisis avanzado" + "enrichments": "Análisis avanzado", + "triggers": "Disparadores", + "roles": "Rols", + "cameraManagement": "Administración", + "cameraReview": "Revisar" }, "dialog": { "unsavedChanges": { @@ -44,7 +50,15 @@ "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" + "title": "Panel en directo", + "displayCameraNames": { + "label": "Siempre mostrar nombres de las Camaras", + "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.", @@ -72,7 +86,7 @@ "title": "Diseños guardados", "clearAll": "Borrar todos los diseños" }, - "title": "Configuración general", + "title": "Ajustes de Interfaz de Usuario", "toast": { "success": { "clearStoredLayout": "Diseño almacenado eliminado para {{cameraName}}", @@ -178,6 +192,44 @@ "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": { @@ -188,7 +240,8 @@ "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." + "mustNotContainPeriod": "El nombre de la zona no debe contener puntos.", + "mustHaveAtLeastOneLetter": "El nombre de la Zona debe contener al menos una letra." } }, "distance": { @@ -254,7 +307,7 @@ "name": { "title": "Nombre", "inputPlaceHolder": "Introduce un nombre…", - "tips": "El nombre debe tener al menos 2 caracteres y no debe ser el nombre de una cámara ni de otra zona." + "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.", @@ -282,7 +335,7 @@ "point_other": "{{count}} puntos", "allObjects": "Todos los objetos", "toast": { - "success": "La zona ({{zoneName}}) ha sido guardada. Reinicia Frigate para aplicar los cambios." + "success": "La zona ({{zoneName}}) ha sido guardada." } }, "toast": { @@ -316,8 +369,8 @@ }, "toast": { "success": { - "noName": "La máscara de movimiento ha sido guardada. Reinicia Frigate para aplicar los cambios.", - "title": "{{polygonName}} ha sido guardado. Reinicia Frigate para aplicar los cambios." + "noName": "La máscara de movimiento ha sido guardada.", + "title": "{{polygonName}} ha sido guardado." } }, "documentTitle": "Editar Máscara de Movimiento - Frigate", @@ -342,8 +395,8 @@ }, "toast": { "success": { - "noName": "La máscara de objetos ha sido guardada. Reinicia Frigate para aplicar los cambios.", - "title": "{{polygonName}} ha sido guardado. Reinicia Frigate para aplicar los cambios." + "noName": "La máscara de objetos ha sido guardada.", + "title": "{{polygonName}} ha sido guardado." } }, "point_one": "{{count}} punto", @@ -423,6 +476,19 @@ "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": { @@ -452,7 +518,7 @@ "role": "Rol", "noUsers": "No se encontraron usuarios.", "changeRole": "Cambiar el rol del usuario", - "password": "Contraseña", + "password": "Restablecer Contraseña", "deleteUser": "Eliminar usuario" }, "dialog": { @@ -477,7 +543,16 @@ "veryStrong": "Muy fuerte" }, "match": "Las contraseñas coinciden", - "notMatch": "Las contraseñas no coinciden" + "notMatch": "Las contraseñas no coinciden", + "show": "Mostrar contraseña", + "hide": "Ocultar contraseña", + "requirements": { + "title": "Requisitos de contraseña:", + "length": "Al menos 8 caracteres", + "uppercase": "Al menos una mayúscula", + "digit": "Al menos un número", + "special": "Al menos un caracter especial (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "Nueva contraseña", @@ -487,14 +562,23 @@ } }, "usernameIsRequired": "Se requiere el nombre de usuario", - "passwordIsRequired": "Se requiere contraseña" + "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" + "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.", @@ -510,7 +594,8 @@ "adminDesc": "Acceso completo a todas las funciones.", "viewerDesc": "Limitado a paneles en vivo, revisión, exploración y exportaciones únicamente.", "viewer": "Espectador", - "admin": "Administrador" + "admin": "Administrador", + "customDesc": "Rol personalizado con acceso a cámaras." }, "select": "Selecciona un rol" }, @@ -520,7 +605,7 @@ "desc": "Esta acción no se puede deshacer. Esto eliminará permanentemente la cuenta de usuario y eliminará todos los datos asociados." } }, - "updatePassword": "Actualizar contraseña" + "updatePassword": "Restablecer contraseña" }, "notification": { "title": "Notificaciones", @@ -683,5 +768,460 @@ "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 con \"rtsp://\". Se requiere configuración manual para transmisiones de cámara sin 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." + } + } + }, + "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": "Habilitar/deshabilitar cámaras", + "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." + }, + "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" + } + } + }, + "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. 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. 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." + } + } } } diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json index 0aaade626..0b441592e 100644 --- a/web/public/locales/es/views/system.json +++ b/web/public/locales/es/views/system.json @@ -42,7 +42,8 @@ "inferenceSpeed": "Velocidad de inferencia del detector", "cpuUsage": "Uso de CPU del Detector", "memoryUsage": "Uso de Memoria del Detector", - "temperature": "Detector de Temperatura" + "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", @@ -75,12 +76,22 @@ }, "gpuMemory": "Memoria de GPU", "npuMemory": "Memoria de NPU", - "npuUsage": "Uso de NPU" + "npuUsage": "Uso 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." + } }, "otherProcesses": { "title": "Otros Procesos", "processCpuUsage": "Uso de CPU del Proceso", - "processMemoryUsage": "Uso de Memoria del Proceso" + "processMemoryUsage": "Uso de Memoria del Proceso", + "series": { + "go2rtc": "go2rtc", + "recording": "grabación", + "review_segment": "revisar segmento" + } } }, "storage": { @@ -102,6 +113,10 @@ "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." } }, "cameras": { @@ -164,9 +179,19 @@ "plate_recognition": "Reconocimiento de Matrículas", "yolov9_plate_detection": "Detección de Matrículas YOLOv9", "image_embedding": "Incrustación de Imágenes", - "yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9" + "yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9", + "review_description": "Revisión de descripción", + "review_description_speed": "Velocidad de revisión de la descripción", + "review_description_events_per_second": "Revisión de la descripción", + "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": "Clasificacion de eventos por segundo de {{name}}" }, - "title": "Enriquecimientos" + "title": "Enriquecimientos", + "averageInf": "Tiempo promedio de inferencia" }, "stats": { "ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)", @@ -175,6 +200,7 @@ "reindexingEmbeddings": "Reindexando incrustaciones ({{processed}}% completado)", "detectIsSlow": "{{detect}} es lento ({{speed}} ms)", "cameraIsOffline": "{{camera}} está desconectada", - "detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)" + "detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)", + "shmTooLow": "Asignación de /dev/shm ({{total}} MB) debe aumentarse al menos a {{min}} MB." } } diff --git a/web/public/locales/et/audio.json b/web/public/locales/et/audio.json new file mode 100644 index 000000000..b0dfec660 --- /dev/null +++ b/web/public/locales/et/audio.json @@ -0,0 +1,117 @@ +{ + "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" +} diff --git a/web/public/locales/et/common.json b/web/public/locales/et/common.json new file mode 100644 index 000000000..ae2d13944 --- /dev/null +++ b/web/public/locales/et/common.json @@ -0,0 +1,298 @@ +{ + "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" + }, + "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)" + }, + "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" + }, + "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" + }, + "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" + } + } + }, + "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" + } +} diff --git a/web/public/locales/et/components/auth.json b/web/public/locales/et/components/auth.json new file mode 100644 index 000000000..f5588cd25 --- /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 000000000..e5df620ec --- /dev/null +++ b/web/public/locales/et/components/camera.json @@ -0,0 +1,86 @@ +{ + "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." + }, + "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" + } +} diff --git a/web/public/locales/et/components/dialog.json b/web/public/locales/et/components/dialog.json new file mode 100644 index 000000000..946142d8a --- /dev/null +++ b/web/public/locales/et/components/dialog.json @@ -0,0 +1,122 @@ +{ + "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" + } + }, + "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" + } + }, + "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 000000000..0df74f6d0 --- /dev/null +++ b/web/public/locales/et/components/filter.json @@ -0,0 +1,88 @@ +{ + "filter": "Filter", + "trackedObjectDelete": { + "toast": { + "error": "Jälgitavate objektide kustutamine ei õnnestunud: {{errorMessage}}", + "success": "Jälgitavate objektide kustutamine õnnestus." + } + }, + "cameras": { + "all": { + "title": "Kõik kaamerad", + "short": "Kaamerad" + } + }, + "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" + }, + "gridColumns": { + "title": "Ruudustiku veerud", + "desc": "Vali ruudustikus kuvatavate veergude arv." + }, + "searchSource": { + "options": { + "thumbnailImage": "Pisipilt", + "description": "Kirjeldus" + } + } + } + }, + "logSettings": { + "loading": { + "title": "Laadin" + }, + "disableLogStreaming": "Keela logi voogedastus", + "allLogs": "Kõik logid" + }, + "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" + } +} diff --git a/web/public/locales/et/components/icons.json b/web/public/locales/et/components/icons.json new file mode 100644 index 000000000..af0569f46 --- /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 000000000..127c8c7f8 --- /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 000000000..76d41dd28 --- /dev/null +++ b/web/public/locales/et/components/player.json @@ -0,0 +1,51 @@ +{ + "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?" + }, + "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" + } + } +} diff --git a/web/public/locales/et/objects.json b/web/public/locales/et/objects.json new file mode 100644 index 000000000..19830deaf --- /dev/null +++ b/web/public/locales/et/objects.json @@ -0,0 +1,120 @@ +{ + "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" +} diff --git a/web/public/locales/et/views/classificationModel.json b/web/public/locales/et/views/classificationModel.json new file mode 100644 index 000000000..93db04cba --- /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": "Skoor 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 000000000..56371cd04 --- /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 000000000..75e4a3d5c --- /dev/null +++ b/web/public/locales/et/views/events.json @@ -0,0 +1,65 @@ +{ + "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": "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" + }, + "calendarFilter": { + "last24Hours": "Viimased 24 tundi" + }, + "objectTrack": { + "clickToSeek": "Klõpsa siia ajapunkti kerimiseks", + "trackedPoint": "Jälgitav punkt" + } +} diff --git a/web/public/locales/et/views/explore.json b/web/public/locales/et/views/explore.json new file mode 100644 index 000000000..76592a97d --- /dev/null +++ b/web/public/locales/et/views/explore.json @@ -0,0 +1,74 @@ +{ + "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" + }, + "title": "Jälgimise üksikasjad", + "noImageFound": "Selle ajatempli kohta ei leidu pilti.", + "createObjectMask": "Loo objektimask" + }, + "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." + } + }, + "snapshotScore": { + "label": "Hetkvõttete punktiskoor" + }, + "regenerateFromSnapshot": "Loo uuesti hetkvõttest", + "timestamp": "Ajatampel" + } +} diff --git a/web/public/locales/et/views/exports.json b/web/public/locales/et/views/exports.json new file mode 100644 index 000000000..56814537e --- /dev/null +++ b/web/public/locales/et/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Eksport Frigate'ist", + "search": "Otsi", + "noExports": "Eksporditud sisu ei leidu", + "deleteExport": "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" + }, + "toast": { + "error": { + "renameExportFailed": "Eksporditud sisu nime muutmine ei õnnestunud: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/et/views/faceLibrary.json b/web/public/locales/et/views/faceLibrary.json new file mode 100644 index 000000000..42c795a06 --- /dev/null +++ b/web/public/locales/et/views/faceLibrary.json @@ -0,0 +1,38 @@ +{ + "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." + }, + "documentTitle": "Näoteek - Frigate", + "createFaceLibrary": { + "new": "Lisa uus nägu" + }, + "deleteFaceLibrary": { + "title": "Kustuta nimi" + }, + "toast": { + "error": { + "addFaceLibraryFailed": "Näo sidumine nimega 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." + } + }, + "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" + } +} diff --git a/web/public/locales/et/views/live.json b/web/public/locales/et/views/live.json new file mode 100644 index 000000000..9ba1ba125 --- /dev/null +++ b/web/public/locales/et/views/live.json @@ -0,0 +1,134 @@ +{ + "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" + }, + "documentTitle": "Otseülekanne - Frigate", + "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" + }, + "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" + }, + "detect": { + "enable": "Lülita tuvastamine sisse", + "disable": "Lülita tuvastamine välja" + }, + "recording": { + "enable": "Lülita salvestamine sisse", + "disable": "Lülita salvestamine välja" + }, + "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." + } + }, + "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." + } + }, + "noCameras": { + "buttonText": "Lisa kaamera", + "restricted": { + "title": "Ühtegi kaamerat pole saadaval", + "description": "Sul pole õigust ühegi selle grupi kaamera vaatamiseks." + } + } +} diff --git a/web/public/locales/et/views/recording.json b/web/public/locales/et/views/recording.json new file mode 100644 index 000000000..57ed97509 --- /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/search.json b/web/public/locales/et/views/search.json new file mode 100644 index 000000000..52b917d22 --- /dev/null +++ b/web/public/locales/et/views/search.json @@ -0,0 +1,23 @@ +{ + "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" + }, + "filter": { + "label": { + "has_snapshot": "Leidub hetkvõte", + "cameras": "Kaamerad", + "labels": "Sildid", + "zones": "Tsoonid", + "sub_labels": "Alamsildid" + } + } +} diff --git a/web/public/locales/et/views/settings.json b/web/public/locales/et/views/settings.json new file mode 100644 index 000000000..c3398cf7b --- /dev/null +++ b/web/public/locales/et/views/settings.json @@ -0,0 +1,193 @@ +{ + "cameraWizard": { + "step1": { + "password": "Salasõna", + "passwordPlaceholder": "Valikuline", + "customUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht", + "connectionSettings": "Ühenduse seadistused" + }, + "step3": { + "streamUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht" + }, + "steps": { + "probeOrSnapshot": "Võta proov või tee hetkvõte" + }, + "step2": { + "testing": { + "fetchingSnapshot": "Laadin kaamera hetkvõtet alla..." + } + } + }, + "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" + }, + "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 8 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" + } + }, + "createUser": { + "confirmPassword": "Palun kinnita oma uus salasõna" + }, + "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" + } + } + }, + "debug": { + "boundingBoxes": { + "desc": "Näita jälgitavate objektide ümber märgiskaste" + } + }, + "documentTitle": { + "default": "Seadistused - Frigate", + "authentication": "Autentimise seadistused - Frigate", + "cameraReview": "Kaamerate kordusvaatuste seadistused - Frigate", + "general": "Kasutajaliidese 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": "Kasutajaliidese seadistused", + "cameraGroupStreaming": { + "clearAll": "Kustuta kõik voogedastuse seadistused" + }, + "liveDashboard": { + "title": "Töölaud reaalajas", + "automaticLiveView": { + "label": "Automaatne otseülekande vaade" + } + } + }, + "cameraManagement": { + "backToSettings": "Tagasi kaameraseadistuste juurde" + }, + "notification": { + "notificationSettings": { + "title": "Teavituste seadistused" + }, + "globalSettings": { + "title": "Üldseadistused" + }, + "deviceSpecific": "Seadmekohased seadistused", + "toast": { + "success": { + "settingSaved": "Teavituste seadistused on salvestatud." + } + } + }, + "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" + } + } + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + }, + "motionMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + }, + "objectMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + } + }, + "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." + } + } + }, + "menu": { + "ui": "Kasutajaliides", + "cameraManagement": "Haldus", + "masksAndZones": "Maskid ja tsoonid", + "triggers": "Päästikud", + "debug": "Silumine ja veaotsing", + "users": "Kasutajad", + "roles": "Rollid", + "notifications": "Teavitused", + "frigateplus": "Frigate+" + }, + "dialog": { + "unsavedChanges": { + "title": "Sul on salvestamata muudatusi.", + "desc": "Kas soovid muudatused enne jätkamist salvestada?" + } + }, + "cameraSetting": { + "camera": "Kaamera", + "noCamera": "Kaamerat pole" + } +} diff --git a/web/public/locales/et/views/system.json b/web/public/locales/et/views/system.json new file mode 100644 index 000000000..b3bbb33aa --- /dev/null +++ b/web/public/locales/et/views/system.json @@ -0,0 +1,17 @@ +{ + "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" + } + }, + "title": "Süsteem" +} diff --git a/web/public/locales/fa/audio.json b/web/public/locales/fa/audio.json index 965460f7f..b3e547006 100644 --- a/web/public/locales/fa/audio.json +++ b/web/public/locales/fa/audio.json @@ -23,5 +23,481 @@ "bus": "اتوبوس", "motorcycle": "موتور سیکلت", "train": "قطار", - "bicycle": "دوچرخه" + "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 index 0967ef424..3b9e02617 100644 --- a/web/public/locales/fa/common.json +++ b/web/public/locales/fa/common.json @@ -1 +1,297 @@ -{} +{ + "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": "زمان پایان نامعتبر است" + }, + "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": "هیچ‌کدام" + }, + "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": "تعلیق‌شده" + }, + "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": "برای زبان از تنظیمات سامانه استفاده کنید" + } + }, + "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": "تنظیم گذرواژه" + } + }, + "toast": { + "copyUrlToClipboard": "نشانی اینترنتی در کلیپ‌بورد کپی شد.", + "save": { + "title": "ذخیره", + "error": { + "title": "ذخیرهٔ تغییرات پیکربندی ناموفق بود: {{errorMessage}}", + "noMessage": "ذخیرهٔ تغییرات پیکربندی ناموفق بود" + } + } + }, + "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" + } +} diff --git a/web/public/locales/fa/components/auth.json b/web/public/locales/fa/components/auth.json index 0967ef424..3c4e021b2 100644 --- a/web/public/locales/fa/components/auth.json +++ b/web/public/locales/fa/components/auth.json @@ -1 +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 index 0967ef424..35f7ec517 100644 --- a/web/public/locales/fa/components/camera.json +++ b/web/public/locales/fa/components/camera.json @@ -1 +1,86 @@ -{} +{ + "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": "نمای پرنده" + } + }, + "debug": { + "options": { + "label": "تنظیمات", + "title": "گزینه‌ها", + "showOptions": "نمایش گزینه‌ها", + "hideOptions": "پنهان کردن گزینه‌ها" + }, + "boundingBox": "کادر محدوده", + "timestamp": "مهر زمانی", + "zones": "ناحیه‌ها", + "mask": "ماسک", + "motion": "حرکت", + "regions": "مناطق" + } +} diff --git a/web/public/locales/fa/components/dialog.json b/web/public/locales/fa/components/dialog.json index 0967ef424..99095fc9d 100644 --- a/web/public/locales/fa/components/dialog.json +++ b/web/public/locales/fa/components/dialog.json @@ -1 +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 index 0967ef424..a742be9f8 100644 --- a/web/public/locales/fa/components/filter.json +++ b/web/public/locales/fa/components/filter.json @@ -1 +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 index 0967ef424..0fa7bec26 100644 --- a/web/public/locales/fa/components/icons.json +++ b/web/public/locales/fa/components/icons.json @@ -1 +1,8 @@ -{} +{ + "iconPicker": { + "selectIcon": "انتخاب آیکون", + "search": { + "placeholder": "جستجو برای آیکون…" + } + } +} diff --git a/web/public/locales/fa/components/input.json b/web/public/locales/fa/components/input.json index 0967ef424..20de89280 100644 --- a/web/public/locales/fa/components/input.json +++ b/web/public/locales/fa/components/input.json @@ -1 +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 index 0967ef424..38e543fb1 100644 --- a/web/public/locales/fa/components/player.json +++ b/web/public/locales/fa/components/player.json @@ -1 +1,51 @@ -{} +{ + "noRecordingsFoundForThisTime": "ویدیویی برای این زمان وجود ندارد", + "noPreviewFound": "پیش‌نمایش پیدا نشد", + "noPreviewFoundFor": "هیچ پیش‌نمایشی برای {{cameraName}} پیدا نشد", + "submitFrigatePlus": { + "title": "این فریم به فریگیت+ ارسال شود؟", + "submit": "ارسال" + }, + "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+ ناموفق بود" + } + } +} diff --git a/web/public/locales/fa/objects.json b/web/public/locales/fa/objects.json index 278086db2..c2ce4e4cf 100644 --- a/web/public/locales/fa/objects.json +++ b/web/public/locales/fa/objects.json @@ -16,5 +16,105 @@ "bird": "پرنده", "cat": "گربه", "dog": "سگ", - "horse": "اسب" + "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/classificationModel.json b/web/public/locales/fa/views/classificationModel.json new file mode 100644 index 000000000..b61d55e4d --- /dev/null +++ b/web/public/locales/fa/views/classificationModel.json @@ -0,0 +1,187 @@ +{ + "button": { + "deleteClassificationAttempts": "حذف تصاویر طبقه بندی", + "renameCategory": "تغییر نام کلاس", + "deleteCategory": "حذف کردن کلاس", + "deleteImages": "حذف کردن عکس ها", + "trainModel": "مدل آموزش", + "addClassification": "افزودن دسته‌بندی", + "deleteModels": "حذف مدل‌ها", + "editModel": "ویرایش مدل" + }, + "toast": { + "success": { + "deletedCategory": "کلاس حذف شده", + "deletedImage": "عکس های حذف شده", + "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 index 0967ef424..c43489dbb 100644 --- a/web/public/locales/fa/views/configEditor.json +++ b/web/public/locales/fa/views/configEditor.json @@ -1 +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 index 0967ef424..cf3ca7871 100644 --- a/web/public/locales/fa/views/events.json +++ b/web/public/locales/fa/views/events.json @@ -1 +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 index 0967ef424..d532878c4 100644 --- a/web/public/locales/fa/views/explore.json +++ b/web/public/locales/fa/views/explore.json @@ -1 +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 index 0967ef424..46aec6287 100644 --- a/web/public/locales/fa/views/exports.json +++ b/web/public/locales/fa/views/exports.json @@ -1 +1,23 @@ -{} +{ + "search": "یافتن", + "documentTitle": "گرفتن خروجی - فریگیت", + "noExports": "هیچ خروجی یافت نشد", + "deleteExport": "حذف خروجی", + "deleteExport.desc": "آیا مطمئن هستید که می‌خواهید {{exportName}} را حذف کنید؟", + "editExport": { + "title": "تغییر نام خروجی", + "desc": "یک نام جدید برای این خروجی وارد کنید.", + "saveExport": "ذخیرهٔ خروجی" + }, + "tooltip": { + "shareExport": "اشتراک‌گذاری خروجی", + "downloadVideo": "دانلود ویدئو", + "editName": "ویرایش نام", + "deleteExport": "حذف خروجی" + }, + "toast": { + "error": { + "renameExportFailed": "تغییر نام خروجی ناموفق بود: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/fa/views/faceLibrary.json b/web/public/locales/fa/views/faceLibrary.json index 0967ef424..4cf24c268 100644 --- a/web/public/locales/fa/views/faceLibrary.json +++ b/web/public/locales/fa/views/faceLibrary.json @@ -1 +1,90 @@ -{} +{ + "description": { + "addFace": "با بارگزاری اولین عکستان، یک مجموعه جدید به کتابخانه چهره اضافه کنید.", + "placeholder": "نامی برای این مجموعه وارد کنید", + "invalidName": "نام نامعتبر، نام ها فقط می توانند شامل حروف، اعداد، فاصله، آپستروف، زیرخط و خط فاصله باشند." + }, + "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": "تلاشِ اخیر برای تشخیص چهره وجود ندارد" + }, + "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}}) به‌روزرسانی شد." + }, + "error": { + "uploadingImageFailed": "آپلود تصویر ناموفق بود: {{errorMessage}}", + "addFaceLibraryFailed": "تنظیم نام چهره ناموفق بود: {{errorMessage}}", + "deleteFaceFailed": "حذف ناموفق بود: {{errorMessage}}", + "deleteNameFailed": "حذف نام ناموفق بود: {{errorMessage}}", + "renameFaceFailed": "تغییر نام چهره ناموفق بود: {{errorMessage}}", + "trainFailed": "آموزش ناموفق بود: {{errorMessage}}", + "updateFaceScoreFailed": "به‌روزرسانی امتیاز چهره ناموفق بود: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/fa/views/live.json b/web/public/locales/fa/views/live.json index 0967ef424..383da433c 100644 --- a/web/public/locales/fa/views/live.json +++ b/web/public/locales/fa/views/live.json @@ -1 +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/recording.json b/web/public/locales/fa/views/recording.json index 0967ef424..a7a9a133c 100644 --- a/web/public/locales/fa/views/recording.json +++ b/web/public/locales/fa/views/recording.json @@ -1 +1,12 @@ -{} +{ + "filter": "فیلتر", + "export": "خروجی گرفتن", + "calendar": "تقویم", + "filters": "فیلترها", + "toast": { + "error": { + "noValidTimeSelected": "بازهٔ زمانی معتبری انتخاب نشده است", + "endTimeMustAfterStartTime": "زمان پایان باید بعد از زمان شروع باشد" + } + } +} diff --git a/web/public/locales/fa/views/search.json b/web/public/locales/fa/views/search.json index 0967ef424..007abe106 100644 --- a/web/public/locales/fa/views/search.json +++ b/web/public/locales/fa/views/search.json @@ -1 +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 index 0967ef424..d2f7ce17b 100644 --- a/web/public/locales/fa/views/settings.json +++ b/web/public/locales/fa/views/settings.json @@ -1 +1,1069 @@ -{} +{ + "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": "فریگیت+" + }, + "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 سریع‌تر اجرا می‌شود، با تفاوت بسیار ناچیز در کیفیت embedding.", + "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}} ( {{label}})" + }, + "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 index 0967ef424..090d4a97f 100644 --- a/web/public/locales/fa/views/system.json +++ b/web/public/locales/fa/views/system.json @@ -1 +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 index 1623e89bd..f0665039f 100644 --- a/web/public/locales/fi/audio.json +++ b/web/public/locales/fi/audio.json @@ -56,7 +56,110 @@ "cough": "Yskä", "sneeze": "Niistää", "throat_clearing": "Kurkun selvittäminen", - "sniff": "Poimi", - "run": "Käynnistä", - "shuffle": "Sekoitus" + "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 index f76eb0e67..5cebc8939 100644 --- a/web/public/locales/fi/common.json +++ b/web/public/locales/fi/common.json @@ -39,7 +39,10 @@ "minute_one": "{{time}}minuutti", "minute_other": "{{time}}minuuttia", "second_one": "{{time}}sekuntti", - "second_other": "{{time}}sekunttia" + "second_other": "{{time}}sekunttia", + "formattedTimestampHourMinute": { + "24hour": "HH:mm" + } }, "pagination": { "next": { @@ -168,5 +171,6 @@ "length": { "feet": "jalka" } - } + }, + "readTheDocumentation": "Lue dokumentaatio" } diff --git a/web/public/locales/fi/components/auth.json b/web/public/locales/fi/components/auth.json index 5ce3ffa02..f81993d86 100644 --- a/web/public/locales/fi/components/auth.json +++ b/web/public/locales/fi/components/auth.json @@ -1,7 +1,7 @@ { "form": { "password": "Salasana", - "user": "Käyttäjä", + "user": "Käyttäjänimi", "login": "Kirjaudu", "errors": { "usernameRequired": "Käyttäjänimi vaaditaan", diff --git a/web/public/locales/fi/components/camera.json b/web/public/locales/fi/components/camera.json index 9dae4c5ed..a641ca65e 100644 --- a/web/public/locales/fi/components/camera.json +++ b/web/public/locales/fi/components/camera.json @@ -66,7 +66,8 @@ }, "stream": "Kuvavirta", "placeholder": "Valitse kuvavirta" - } + }, + "birdseye": "Linnun silmä" } }, "debug": { diff --git a/web/public/locales/fi/components/dialog.json b/web/public/locales/fi/components/dialog.json index 9a1ca575d..819e4a55e 100644 --- a/web/public/locales/fi/components/dialog.json +++ b/web/public/locales/fi/components/dialog.json @@ -73,5 +73,15 @@ "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 index 5a21e5424..c3058bd29 100644 --- a/web/public/locales/fi/components/filter.json +++ b/web/public/locales/fi/components/filter.json @@ -56,7 +56,36 @@ "cameras": { "label": "Kameran suodattimet", "all": { - "title": "Kaikki kamerat" + "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/views/classificationModel.json b/web/public/locales/fi/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/fi/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/fi/views/configEditor.json b/web/public/locales/fi/views/configEditor.json index 472c59e37..96990e140 100644 --- a/web/public/locales/fi/views/configEditor.json +++ b/web/public/locales/fi/views/configEditor.json @@ -12,5 +12,7 @@ }, "configEditor": "Konfiguraatioeditori", "copyConfig": "Kopioi konfiguraatio", - "saveAndRestart": "Tallenna & uudelleenkäynnistä" + "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 index 638a05f7a..57eb44a80 100644 --- a/web/public/locales/fi/views/events.json +++ b/web/public/locales/fi/views/events.json @@ -34,5 +34,7 @@ "label": "Näytä uudet katselmoitavat kohteet", "button": "Uudet katselmoitavat kohteet" }, - "camera": "Kamera" + "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 index c6950c941..25743e470 100644 --- a/web/public/locales/fi/views/explore.json +++ b/web/public/locales/fi/views/explore.json @@ -7,7 +7,45 @@ "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": { @@ -28,7 +66,8 @@ "setup": { "visionModel": "Vision-malli", "textModel": "Tekstimalli", - "textTokenizer": "Tekstin osioija" + "textTokenizer": "Tekstin osioija", + "visionModelFeatureExtractor": "Näkömallin piirreluokkain" }, "tips": { "documentation": "Lue dokumentaatio", @@ -90,6 +129,27 @@ "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/faceLibrary.json b/web/public/locales/fi/views/faceLibrary.json index 041c7324f..dc69f3694 100644 --- a/web/public/locales/fi/views/faceLibrary.json +++ b/web/public/locales/fi/views/faceLibrary.json @@ -26,7 +26,8 @@ "toast": { "success": { "deletedFace_one": "{{count}} kasvo poistettu onnistuneesti.", - "deletedFace_other": "{{count}} kasvoa poistettu onnistuneesti." + "deletedFace_other": "{{count}} kasvoa poistettu onnistuneesti.", + "uploadedImage": "Kuva ladattu onnistuneesti." } }, "selectItem": "Valitse {{item}}", @@ -60,6 +61,22 @@ "desc": "Anna uusi nimi tälle {{name}}" }, "button": { - "deleteFaceAttempts": "Poista kasvot" - } + "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 index 69c0d23bf..d38703565 100644 --- a/web/public/locales/fi/views/live.json +++ b/web/public/locales/fi/views/live.json @@ -43,7 +43,15 @@ "label": "Napsauta kehystä keskittääksesi PTZ-kamera" } }, - "presets": "PTZ-kameroiden esiasetukset" + "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", @@ -135,7 +143,8 @@ "recording": "Nauhoitus", "snapshots": "Tilannekuvat", "audioDetection": "Äänen tunnistus", - "autotracking": "Automaattinen seuranta" + "autotracking": "Automaattinen seuranta", + "transcription": "Äänitranskriptio" }, "history": { "label": "Näytä historiallista materiaalia" @@ -154,5 +163,9 @@ "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/search.json b/web/public/locales/fi/views/search.json index fab605088..887c9e09e 100644 --- a/web/public/locales/fi/views/search.json +++ b/web/public/locales/fi/views/search.json @@ -44,7 +44,14 @@ }, "tips": { "desc": { - "exampleLabel": "Esimerkki:" + "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ö" }, @@ -58,5 +65,8 @@ "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 index 23b910dda..cda27193f 100644 --- a/web/public/locales/fi/views/settings.json +++ b/web/public/locales/fi/views/settings.json @@ -22,7 +22,8 @@ "debug": "Debuggaus", "motionTuner": "Liikesäädin", "notifications": "Ilmoitukset", - "enrichments": "Rikasteet" + "enrichments": "Rikasteet", + "triggers": "Laukaisimet" }, "dialog": { "unsavedChanges": { @@ -176,7 +177,14 @@ "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": { @@ -415,6 +423,11 @@ "placeholder": "Syötä käyttäjätunnus", "title": "Käyttäjätunnus" } + }, + "changeRole": { + "roleInfo": { + "admin": "Ylläpitäjä" + } } } }, @@ -427,5 +440,140 @@ "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 index 5000e45c6..04952692e 100644 --- a/web/public/locales/fi/views/system.json +++ b/web/public/locales/fi/views/system.json @@ -55,6 +55,13 @@ }, "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}}" } } }, diff --git a/web/public/locales/fr/audio.json b/web/public/locales/fr/audio.json index b773f026b..b34615853 100644 --- a/web/public/locales/fr/audio.json +++ b/web/public/locales/fr/audio.json @@ -1,10 +1,10 @@ { - "speech": "Conversation", + "speech": "Parole", "babbling": "Babillage", - "yell": "Crier", + "yell": "Cri", "bicycle": "Vélo", "car": "Voiture", - "bellow": "Ci-dessous", + "bellow": "Beuglement", "whispering": "Chuchotement", "laughter": "Rires", "snicker": "Ricanement", @@ -13,7 +13,7 @@ "bus": "Bus", "train": "Train", "motorcycle": "Moto", - "whoop": "Cri", + "whoop": "Cri strident", "sigh": "Soupir", "singing": "Chant", "choir": "Chorale", @@ -22,7 +22,7 @@ "mantra": "Mantra", "child_singing": "Chant d'enfant", "bird": "Oiseau", - "cat": "chat", + "cat": "Chat", "synthetic_singing": "Chant synthétique", "rapping": "Rap", "horse": "Cheval", @@ -31,15 +31,15 @@ "whistling": "Sifflement", "breathing": "Respiration", "snoring": "Ronflement", - "gasp": "Souffle", + "gasp": "Souffle coupé", "pant": "halètement", - "snort": "Reniflement", + "snort": "Ébrouement", "camera": "Caméra", - "cough": "Toussotement", + "cough": "Toux", "groan": "Gémissement", "grunt": "Grognement", - "throat_clearing": "Éclaircissement de la gorge", - "wheeze": "Respiration bruyante", + "throat_clearing": "Raclement de gorge", + "wheeze": "Respiration sifflante", "sneeze": "Éternuement", "sniff": "Reniflement", "chewing": "Mastication", @@ -72,7 +72,7 @@ "burping": "Rots", "fart": "Pet", "crowd": "Foule", - "children_playing": "Enfants en train de jouer", + "children_playing": "Jeux d'enfants", "animal": "Animal", "bark": "Aboiement", "pig": "Cochon", @@ -80,7 +80,7 @@ "chicken": "Poulet", "turkey": "Dinde", "duck": "Canard", - "goose": "Dindon", + "goose": "Oie", "wild_animals": "Animaux Sauvages", "crow": "Corbeau", "dogs": "Chiens", @@ -99,31 +99,31 @@ "vehicle": "Véhicule", "skateboard": "Skateboard", "door": "Porte", - "blender": "Mixer", - "hair_dryer": "Sèche cheveux", + "blender": "Mixeur", + "hair_dryer": "Sèche-cheveux", "toothbrush": "Brosse à dents", - "sink": "Lavabo", - "scissors": "Paire de ciseaux", + "sink": "Évier", + "scissors": "Ciseaux", "humming": "Bourdonnement", - "shuffle": "Mélanger", - "footsteps": "Pas", + "shuffle": "Pas traînants", + "footsteps": "Bruits de pas", "hiccup": "Hoquet", "finger_snapping": "Claquement de doigts", "clapping": "Claquements", "applause": "Applaudissements", "heartbeat": "Battements de coeur", - "cheering": "Applaudissement", + "cheering": "Acclamations", "electric_shaver": "Rasoir électrique", "truck": "Camion", - "run": "Démarrer", + "run": "Course", "biting": "Mordre", "stomach_rumble": "Gargouillements d'estomac", "hands": "Mains", "heart_murmur": "Souffle au cœur", - "chatter": "Bavarder", + "chatter": "Bavardage", "pets": "Animaux de compagnie", - "yip": "Ouais", - "howl": "Hurler", + "yip": "Jappement", + "howl": "Hurlement", "growling": "Grondement", "whimper_dog": "Gémissements de chien", "purr": "Ronronnements", @@ -132,8 +132,8 @@ "livestock": "Bétail", "neigh": "Hennissement", "quack": "Coin-coin", - "honk": "Klaxon", - "roaring_cats": "Feulements", + "honk": "Cacardement", + "roaring_cats": "Rugissement de félins", "roar": "Rugissements", "chirp": "Gazouillis", "squawk": "Braillement", @@ -191,7 +191,7 @@ "steelpan": "Pan", "orchestra": "Orchestre", "brass_instrument": "Cuivres", - "french_horn": "Cor français", + "french_horn": "Cor d'harmonie", "trumpet": "Trompette", "bowed_string_instrument": "Instrument à cordes frottées", "string_section": "Section des cordes", @@ -247,7 +247,7 @@ "sad_music": "Musique triste", "tender_music": "Musique tendre", "exciting_music": "Musique stimulante", - "angry_music": "Musique énervée", + "angry_music": "Musique agressive", "scary_music": "Musique effrayante", "wind": "Vent", "rustling_leaves": "Bruissements de feuilles", @@ -277,7 +277,7 @@ "skidding": "Dérapage", "tire_squeal": "Crissements de pneu", "car_passing_by": "Passage de voiture", - "race_car": "Course de voitures", + "race_car": "Voiture de course", "air_brake": "Frein pneumatique", "air_horn": "Klaxon à air", "reversing_beeps": "Bips de marche arrière", @@ -311,7 +311,7 @@ "squeak": "Grincement", "cupboard_open_or_close": "Ouverture ou fermeture de placard", "drawer_open_or_close": "Ouverture ou fermeture de tiroir", - "dishes": "Plats", + "dishes": "Bruit de vaisselle", "cutlery": "Couverts", "chopping": "Hacher", "frying": "Friture", @@ -324,7 +324,7 @@ "zipper": "Fermeture éclair", "keys_jangling": "Tintements de clés", "coin": "Pièce de monnaie", - "shuffling_cards": "Mélange de cartes", + "shuffling_cards": "Battement de cartes", "typing": "Frappe au clavier", "typewriter": "Machine à écrire", "writing": "Écriture", @@ -414,16 +414,90 @@ "idling": "Ralenti", "radio": "Radio", "telephone": "Téléphone", - "bow_wow": "Ouaf ouaf", + "bow_wow": "Aboiement", "hiss": "Sifflement", "clip_clop": "Clic-clac", "cattle": "Bétail", "moo": "Meuglement", "cowbell": "Clochette", "oink": "Grouin-grouin", - "bleat": "Bêler", + "bleat": "Bêlement", "fowl": "Volaille", "cluck": "Gloussement", "cock_a_doodle_doo": "Cocorico", - "gobble": "Glouglou" + "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 index 5ed9f65a9..a1132a01e 100644 --- a/web/public/locales/fr/common.json +++ b/web/public/locales/fr/common.json @@ -1,6 +1,6 @@ { "time": { - "untilForRestart": "Jusqu'au redémarrage de Frigate.", + "untilForRestart": "Jusqu'au redémarrage de Frigate", "untilRestart": "Jusqu'au redémarrage", "untilForTime": "Jusqu'à {{time}}", "justNow": "À l'instant", @@ -22,10 +22,10 @@ "pm": "PM", "am": "AM", "yr": "{{time}} a", - "year_one": "{{time}} année", - "year_many": "{{time}} années", - "year_other": "{{time}} années", - "mo": "{{time}} m", + "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", @@ -33,7 +33,7 @@ "second_one": "{{time}} seconde", "second_many": "{{time}} secondes", "second_other": "{{time}} secondes", - "m": "{{time}} mn", + "m": "{{time}} min", "hour_one": "{{time}} heure", "hour_many": "{{time}} heures", "hour_other": "{{time}} heures", @@ -65,29 +65,32 @@ }, "formattedTimestampHourMinute": { "24hour": "HH:mm", - "12hour": "HH:mm aaa" + "12hour": "HH:mm" }, "formattedTimestampMonthDay": "d MMM", "formattedTimestampFilename": { - "12hour": "dd-MM-yy-HH-mm-ss-a", + "12hour": "dd-MM-yy-HH-mm-ss", "24hour": "dd-MM-yy-HH-mm-ss" }, "formattedTimestampMonthDayHourMinute": { - "12hour": "d MMM, HH:mm aaa", + "12hour": "d MMM, HH:mm", "24hour": "d MMM, HH:mm" }, "formattedTimestampHourMinuteSecond": { "24hour": "HH:mm:ss", - "12hour": "HH:mm:ss aaa" + "12hour": "HH:mm:ss" }, "formattedTimestampMonthDayYearHourMinute": { - "12hour": "d MMM yyyy, HH:mm aaa", + "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" }, "button": { "apply": "Appliquer", @@ -98,16 +101,16 @@ "close": "Fermer", "copy": "Copier", "back": "Retour", - "history": "Historique", - "pictureInPicture": "Image en incrustation", + "history": "Chronologie", + "pictureInPicture": "Image dans l'image", "twoWayTalk": "Conversation bidirectionnelle", - "off": "Inactif", - "edit": "Editer", + "off": "OFF", + "edit": "Modifier", "copyCoordinates": "Copier les coordonnées", "delete": "Supprimer", "yes": "Oui", "no": "Non", - "unsuspended": "Reprendre", + "unsuspended": "Réactiver", "play": "Lire", "unselect": "Désélectionner", "suspended": "Suspendu", @@ -120,11 +123,12 @@ "next": "Suivant", "exitFullscreen": "Sortir du mode plein écran", "cameraAudio": "Son de la caméra", - "on": "Actif", + "on": "ON", "export": "Exporter", "deleteNow": "Supprimer maintenant", "download": "Télécharger", - "done": "Terminé" + "done": "Terminé", + "continue": "Continuer" }, "menu": { "configuration": "Configuration", @@ -142,14 +146,14 @@ "nl": "Nederlands (Néerlandais)", "sv": "Svenska (Suédois)", "cs": "Čeština (Tchèque)", - "nb": "Norsk Bokmål (Bokmål Norvégien)", + "nb": "Norsk Bokmål (Norvégien Bokmål)", "ko": "한국어 (Coréen)", - "fa": "فارسی (Perse)", + "fa": "فارسی (Persan)", "pl": "Polski (Polonais)", "el": "Ελληνικά (Grec)", "ro": "Română (Roumain)", "hu": "Magyar (Hongrois)", - "he": "עברית (Hebreu)", + "he": "עברית (Hébreu)", "ru": "Русский (Russe)", "de": "Deutsch (Allemand)", "es": "Español (Espagnol)", @@ -162,7 +166,15 @@ "vi": "Tiếng Việt (Vietnamien)", "yue": "粵語 (Cantonais)", "th": "ไทย (Thai)", - "ca": "Català (Catalan)" + "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)" }, "appearance": "Apparence", "darkMode": { @@ -173,7 +185,7 @@ }, "label": "Mode sombre" }, - "review": "Revue d'événements", + "review": "Activités", "explore": "Explorer", "export": "Exporter", "user": { @@ -191,18 +203,18 @@ }, "system": "Système", "help": "Aide", - "configurationEditor": "Editeur de configuration", + "configurationEditor": "Éditeur de configuration", "theme": { "contrast": "Contraste élevé", "blue": "Bleu", "green": "Vert", "nord": "Nord", "red": "Rouge", - "default": "Défaut", + "default": "Par défaut", "label": "Thème", "highcontrast": "Contraste élevé" }, - "systemMetrics": "Indicateurs systèmes", + "systemMetrics": "Métriques du système", "settings": "Paramètres", "withSystem": "Système", "restart": "Redémarrer Frigate", @@ -216,25 +228,26 @@ "allCameras": "Toutes les caméras", "title": "Direct" }, - "uiPlayground": "Gestion de l'interface", + "uiPlayground": "Bac à sable de l'interface", "faceLibrary": "Bibliothèque de visages", - "languages": "Langues" + "languages": "Langues", + "classification": "Classification" }, "toast": { "save": { "title": "Enregistrer", "error": { "noMessage": "Echec lors de l'enregistrement des changements de configuration", - "title": "Echec lors de l'enregistrement des changements de configuration : {{errorMessage}}" + "title": "Échec de l'enregistrement des changements de configuration : {{errorMessage}}" } }, - "copyUrlToClipboard": "Lien copié dans le presse-papier." + "copyUrlToClipboard": "URL copiée dans le presse-papiers" }, "role": { "title": "Rôle", "viewer": "Observateur", "admin": "Administrateur", - "desc": "Les administrateurs accèdent à l'ensemble des fonctionnalités de l'interface Frigate. Les observateurs sont limités à la consultation des caméras, de la revue d'événements, et à l'historique des enregistrements dans l'interface utilisateur." + "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": { @@ -254,13 +267,19 @@ "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 voir cette page." + "desc": "Vous n'avez pas l'autorisation de consulter cette page." }, "label": { - "back": "Retour" + "back": "Retour", + "hide": "Masquer {{item}}", + "show": "Afficher {{item}}", + "ID": "ID", + "none": "Aucun", + "all": "Tous" }, "unit": { "speed": { @@ -270,6 +289,26 @@ "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": ", " } } diff --git a/web/public/locales/fr/components/auth.json b/web/public/locales/fr/components/auth.json index 65e26691b..3e600fb71 100644 --- a/web/public/locales/fr/components/auth.json +++ b/web/public/locales/fr/components/auth.json @@ -1,15 +1,16 @@ { "form": { "password": "Mot de passe", - "login": "Identifiant", + "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": "Un mot de passe est requis", + "passwordRequired": "Mot de passe est requis", "loginFailed": "Échec de l'authentification", - "usernameRequired": "Un nom d'utilisateur est requis", - "rateLimit": "Nombre d'essais dépassé. Réessayez plus tard." - } + "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 index 582b211b5..0e95c70e3 100644 --- a/web/public/locales/fr/components/camera.json +++ b/web/public/locales/fr/components/camera.json @@ -1,38 +1,38 @@ { "group": { - "edit": "Éditer le groupe de caméras", - "label": "Groupe de caméras", + "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": "Confirmer la suppression", + "title": "Confirmez la suppression", "desc": "Êtes-vous sûr de vouloir supprimer le groupe de caméras {{name}} ?" } }, "name": { - "placeholder": "Saisissez un nom…", + "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 période.", - "invalid": "Nom de groupe de caméras invalide." + "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électionner les caméras pour ce groupe." + "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 de flux de caméra", - "title": "Paramètres de flux de {{cameraName}}", - "audioIsUnavailable": "L'audio n'est pas disponible pour ce flux", - "audioIsAvailable": "L'audio est disponible pour ce flux", - "desc": "Modifie les options du flux temps réel pour le tableau de bord de ce groupe de caméras. Ces paramètres sont spécifiques à un périphérique et/ou navigateur.", + "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 ", @@ -40,33 +40,34 @@ } }, "streamMethod": { - "label": "Méthode de streaming", + "label": "Méthode de diffusion", "method": { "noStreaming": { - "label": "Pas de diffusion", - "desc": "Les images provenant de la caméra ne seront mises à jour qu'une fois par minute et il n'y aura pas de diffusion en direct." + "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é)", - "desc": "La diffusion intelligente mettra à jour les images de la caméra une fois par minute lorsqu'aucune activité n'est détectée afin de conserver la bande-passante et les ressources. Quand une activité est détectée, le flux bascule automatiquement en diffusion temps réel." + "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 engendrer une bande-passante élevée et des problèmes de performance. A utiliser avec précaution." + "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" + "placeholder": "Choisissez une méthode de diffusion." }, "compatibilityMode": { "label": "Mode de compatibilité", - "desc": "Activer cette option uniquement si votre flux temps réel affiche des erreurs chromatiques et a une ligne diagonale sur le côté droit de l'image." + "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" - } + "placeholder": "Choisissez un flux." + }, + "birdseye": "Birdseye" } }, "debug": { @@ -79,7 +80,7 @@ "label": "Paramètres", "hideOptions": "Masquer les options" }, - "boundingBox": "Boîte de délimitation", + "boundingBox": "Cadre de détection", "zones": "Zones", "regions": "Régions" } diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json index d92e3ff72..f0b542b70 100644 --- a/web/public/locales/fr/components/dialog.json +++ b/web/public/locales/fr/components/dialog.json @@ -2,8 +2,8 @@ "restart": { "title": "Êtes-vous sûr de vouloir redémarrer Frigate ?", "restarting": { - "title": "Frigate redémarre", - "content": "Actualisation de la page dans {{countdown}} secondes.", + "title": "Redémarrage de Frigate en cours", + "content": "Cette page sera rechargée dans {{countdown}} secondes.", "button": "Forcer l'actualisation maintenant" }, "button": "Redémarrer" @@ -31,10 +31,10 @@ "submitted": "Soumis" }, "question": { - "label": "Confirmez ce libellé pour Frigate+", - "ask_an": "Est-ce que cet objet est un(e) {{label}} ?", - "ask_a": "Est-ce que cet objet est un(e) {{label}} ?", - "ask_full": "Est-ce-que cet objet est un(e) {{translatedLabel}}  ?" + "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}}  ?" } } }, @@ -61,25 +61,26 @@ "selectOrExport": "Sélectionner ou exporter", "toast": { "error": { - "failed": "Échec du démarrage de l'export : {{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" + "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 dans le dossier /exports." + "success": "Exportation démarrée avec succès. Consultez le fichier sur la page des exportations.", + "view": "Vue" }, "select": "Sélectionner", "name": { - "placeholder": "Nommer l'export" + "placeholder": "Nommer l'exportation" }, "export": "Exporter", "fromTimeline": { - "saveExport": "Enregistrer l'export", - "previewExport": "Prévisualiser l'export" + "saveExport": "Enregistrer l'exportation", + "previewExport": "Aperçu de l'exportation" } }, "search": { "saveSearch": { - "desc": "Donnez un nom à cette recherche enregistrée.", + "desc": "Saisissez un nom pour cette recherche enregistrée.", "label": "Enregistrer la recherche", "success": "La recherche ({{searchName}}) a été enregistrée.", "button": { @@ -88,7 +89,7 @@ } }, "overwrite": "{{searchName}} existe déjà. L'enregistrement écrasera la recherche existante.", - "placeholder": "Saisissez un nom pour votre recherche" + "placeholder": "Saisissez un nom pour votre recherche." } }, "streaming": { @@ -102,25 +103,34 @@ }, "showStats": { "label": "Afficher les statistiques du flux", - "desc": "Activez cette option pour montrer les statistiques de diffusion en incrustation sur le flux vidéo de la caméra." + "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 à cet élément de la revue d'événements ?

    Maintenez la touche Maj enfoncée pour éviter cette boîte de dialogue à l'avenir." + "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 éléments de revue d'événements sélectionnés ont été supprimées.", + "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 passé en revue", - "deleteNow": "Supprimer maintenant" + "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 index 567cf81f5..0af924f8b 100644 --- a/web/public/locales/fr/components/filter.json +++ b/web/public/locales/fr/components/filter.json @@ -1,13 +1,13 @@ { "labels": { - "label": "Libellés", + "label": "Étiquettes", "all": { - "title": "Tous les libellés", - "short": "Libellés" + "title": "Toutes les étiquettes", + "short": "Étiquettes" }, "count": "{{count}} Étiquettes", - "count_one": "{{count}} libellé", - "count_other": "{{count}} libellés" + "count_one": "{{count}} étiquette", + "count_other": "{{count}} étiquettes" }, "filter": "Filtre", "zones": { @@ -22,16 +22,16 @@ "title": "Toutes les dates", "short": "Dates" }, - "selectPreset": "Sélectionnez un préréglage…" + "selectPreset": "Sélectionnez un préréglage." }, "more": "Plus de filtres", "reset": { "label": "Réinitialiser les filtres aux valeurs par défaut" }, - "timeRange": "Plage de temps", + "timeRange": "Plage horaire", "subLabels": { - "label": "Sous-libellés", - "all": "Tous les sous-libellés" + "label": "Sous-étiquettes", + "all": "Toutes les sous-étiquettes" }, "score": "Score", "estimatedSpeed": "Vitesse estimée ({{unit}})", @@ -50,9 +50,9 @@ "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": "A un clip vidéo", - "hasSnapshot": "A un instantané", - "label": "Fonctionnalités" + "hasVideoClip": "Avec une séquence vidéo", + "hasSnapshot": "Avec un instantané", + "label": "Caractéristiques" }, "explore": { "settings": { @@ -61,7 +61,7 @@ "title": "Vue par défaut", "summary": "Résumé", "unfilteredGrid": "Grille non filtrée", - "desc": "Lorsqu'aucun filtre n'est sélectionné, affiche un résumé des objets suivis les plus récents par libellé, ou affiche une 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.", @@ -70,7 +70,7 @@ "searchSource": { "label": "Source de recherche", "options": { - "thumbnailImage": "Image de miniature", + "thumbnailImage": "Miniature", "description": "Description" }, "desc": "Choisissez si vous souhaitez rechercher les miniatures ou les descriptions de vos objets suivis." @@ -83,7 +83,7 @@ } }, "review": { - "showReviewed": "Montrer les éléments passés en revue" + "showReviewed": "Afficher les activités traitées" }, "cameras": { "label": "Filtre des caméras", @@ -101,27 +101,41 @@ "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": "Niveau de journalisation du filtre", - "disableLogStreaming": "Désactiver la diffusion des journaux", + "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.", + "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électionner une ou plusieurs plaques d'immatriculation dans la liste." + "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": "Les objets suivis ont été supprimés avec succès.", + "success": "Objets suivis supprimés avec succès.", "error": "Échec de la suppression des objets suivis : {{errorMessage}}" }, - "desc": "Supprimer ces objets suivis {{objectLength}} retirera l'instantané, les représentations numériques enregistrées 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 Historique NE seront PAS supprimées.

    Voulez-vous vraiment continuer ?

    Maintenez la touche Maj enfoncée pour ignorer cette boîte de dialogue à l'avenir." + "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 index f713f2f52..fd5f1f8f6 100644 --- a/web/public/locales/fr/components/icons.json +++ b/web/public/locales/fr/components/icons.json @@ -1,8 +1,8 @@ { "iconPicker": { "search": { - "placeholder": "Rechercher une icône…" + "placeholder": "Rechercher une icône" }, - "selectIcon": "Sélectionnez 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 index 36874788e..0d8130cf5 100644 --- a/web/public/locales/fr/components/input.json +++ b/web/public/locales/fr/components/input.json @@ -3,7 +3,7 @@ "downloadVideo": { "label": "Télécharger la vidéo", "toast": { - "success": "Le téléchargement de la vidéo de votre élément de la revue d'événements a commencé." + "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 index 7dd7346e5..6450c1261 100644 --- a/web/public/locales/fr/components/player.json +++ b/web/public/locales/fr/components/player.json @@ -10,8 +10,8 @@ "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 requis pour ce type de flux en direct.", - "cameraDisabled": "La caméra est désactivée", + "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 :", @@ -37,7 +37,7 @@ "title": "Images perdues :" }, "decodedFrames": "Images décodées :", - "droppedFrameRate": "Proportion d'images perdues :", + "droppedFrameRate": "Taux d'images perdues :", "totalFrames": "Total images :" }, "toast": { diff --git a/web/public/locales/fr/objects.json b/web/public/locales/fr/objects.json index d959a8e42..9c9d5a6cf 100644 --- a/web/public/locales/fr/objects.json +++ b/web/public/locales/fr/objects.json @@ -9,17 +9,17 @@ "boat": "Bateau", "traffic_light": "Feu de circulation", "fire_hydrant": "Bouche d'incendie", - "street_sign": "Plaque de rue", + "street_sign": "Panneau de signalisation", "parking_meter": "Parcmètre", "bench": "Banc", "bird": "Oiseau", - "cat": "chat", + "cat": "Chat", "stop_sign": "Panneau de stop", "dog": "Chien", "horse": "Cheval", "sheep": "Mouton", "cow": "Vache", - "elephant": "Eléphant", + "elephant": "Éléphant", "bear": "Ours", "zebra": "Zèbre", "hat": "Chapeau", @@ -27,10 +27,10 @@ "suitcase": "Valise", "frisbee": "Frisbee", "skis": "Skis", - "snowboard": "Surf des neiges", - "sports_ball": "Ballon des sports", + "snowboard": "Snowboard", + "sports_ball": "Ballon de sport", "kite": "Cerf-volant", - "baseball_bat": "Batte de base-ball", + "baseball_bat": "Batte de baseball", "umbrella": "Parapluie", "giraffe": "Girafe", "eye_glasses": "Lunettes", @@ -42,7 +42,7 @@ "baseball_glove": "Gant de baseball", "skateboard": "Skateboard", "surfboard": "Planche de surf", - "tennis_racket": "Raquette de Tennis", + "tennis_racket": "Raquette de tennis", "plate": "Assiette", "cup": "Tasse", "banana": "Banane", @@ -63,7 +63,7 @@ "toaster": "Grille-pain", "book": "Livre", "teddy_bear": "Ours en peluche", - "blender": "Mixer", + "blender": "Mixeur", "toothbrush": "Brosse à dents", "hair_brush": "Brosse à cheveux", "vehicle": "Véhicule", @@ -92,7 +92,7 @@ "refrigerator": "Réfrigérateur", "bark": "Aboiement", "oven": "Four", - "scissors": "Paire de ciseaux", + "scissors": "Ciseaux", "toilet": "Toilettes", "carrot": "Carotte", "bed": "Lit", @@ -100,11 +100,11 @@ "fork": "Fourchette", "squirrel": "Écureuil", "microwave": "Micro-ondes", - "hair_dryer": "Sèche cheveux", + "hair_dryer": "Sèche-cheveux", "bowl": "Bol", "spoon": "Cuillère", "sandwich": "Sandwich", - "sink": "Lavabo", + "sink": "Évier", "broccoli": "Brocoli", "knife": "Couteau", "nzpost": "NZPost", diff --git a/web/public/locales/fr/views/classificationModel.json b/web/public/locales/fr/views/classificationModel.json new file mode 100644 index 000000000..0926f4cd6 --- /dev/null +++ b/web/public/locales/fr/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "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": "Classe supprimée", + "deletedImage": "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" + }, + "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}}" + } + }, + "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 index 5f88fb94f..0ab9b2c40 100644 --- a/web/public/locales/fr/views/configEditor.json +++ b/web/public/locales/fr/views/configEditor.json @@ -2,7 +2,7 @@ "configEditor": "Éditeur de configuration", "documentTitle": "Éditeur de configuration - Frigate", "copyConfig": "Copier la configuration", - "saveOnly": "Enregistrer seulement", + "saveOnly": "Enregistrer uniquement", "saveAndRestart": "Enregistrer et redémarrer", "toast": { "success": { @@ -12,5 +12,7 @@ "savingError": "Erreur lors de l'enregistrement de la configuration" } }, - "confirm": "Quitter sans enregistrer ?" + "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 index d8d58332c..6baaf9b93 100644 --- a/web/public/locales/fr/views/events.json +++ b/web/public/locales/fr/views/events.json @@ -2,22 +2,26 @@ "detections": "Détections", "motion": { "label": "Mouvement", - "only": "Mouvement seulement" + "only": "Mouvement uniquement" }, "alerts": "Alertes", "allCameras": "Toutes les caméras", "empty": { - "alert": "Il n'y a aucune alerte à passer en revue", - "detection": "Il n'y a aucune détection à passer en revue", - "motion": "Aucune donnée de mouvement trouvée" + "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": "Chronologie", "events": { "label": "Événements", "aria": "Sélectionner les événements", - "noFoundForTimePeriod": "Aucun événement trouvé pour cette plage de temps." + "noFoundForTimePeriod": "Aucun événement n'a été trouvé pour cette plage de temps." }, - "documentTitle": "Revue d'événements -Frigate", + "documentTitle": "Activités - Frigate", "recordings": { "documentTitle": "Enregistrements - Frigate" }, @@ -25,15 +29,40 @@ "last24Hours": "Dernières 24 heures" }, "timeline.aria": "Sélectionner une chronologie", - "markAsReviewed": "Marqué comme passé en revue", + "markAsReviewed": "Marquer comme traitê", "newReviewItems": { - "button": "Nouveaux éléments à passer en revue", - "label": "Afficher les nouveaux éléments de la revue d'événements" + "button": "Nouvelles activités à traiter", + "label": "Afficher les nouvelles activités" }, "camera": "Caméra", - "markTheseItemsAsReviewed": "Marquer ces éléments comme passés en revue", + "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é" + "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 index b42cb5f38..637936450 100644 --- a/web/public/locales/fr/views/explore.json +++ b/web/public/locales/fr/views/explore.json @@ -5,17 +5,17 @@ "title": "L'exploration est indisponible", "embeddingsReindexing": { "estimatedTime": "Temps restant estimé :", - "finishingShortly": "Termine bientôt", - "context": "L'exploration peut être utilisée une fois la réindexation des représentations numériques des objets suivis terminée.", + "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": "Miniatures intégrées : ", - "descriptionsEmbedded": "Descriptions intégrées : ", + "thumbnailsEmbedded": "Embeddings des miniatures : ", + "descriptionsEmbedded": "Embeddings des descriptions  : ", "trackedObjectsProcessed": "Objets suivis traités : " } }, "downloadingModels": { - "context": "Frigate télécharge les modèles de représentations numériques 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.", + "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", @@ -24,48 +24,52 @@ }, "tips": { "documentation": "Lire la documentation", - "context": "Une fois les modèles téléchargés, il est conseillé de réindexer vos objets suivis." + "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érifier les journaux Frigate." + "error": "Une erreur est survenue. Vérifiez les journaux Frigate." } }, "details": { "timestamp": "Horodatage", "item": { - "title": "Détails de l'élément de la revue d'événements", + "title": "Détails de l'activité", "button": { - "share": "Partager cet élément de la revue d'événements", + "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-libellé mis à jour avec succès.", - "updatedLPR": "Plaque d'immatriculation mise à jour avec succès." + "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 du sous-libellé : {{errorMessage}}", - "updatedLPRFailed": "Échec de la mise à jour de la plaque d'immatriculation : {{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 cet élément de la revue d'événements. 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 cet élément de la revue d'événements. 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 cet élément de la revue d'événements. 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 libellés suivants : {{objects}}" + "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'élément de la revue d'événements" + "desc": "Détails de l'activité" }, - "label": "Libellé", + "label": "Étiquette", "editSubLabel": { - "title": "Modifier le sous-libellé", - "desc": "Saisissez un nouveau sous-libellé pour {{label}}", - "descNoLabel": "Entrer un nouveau sous-libellé pour cet objet suivi" + "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 score le plus élevé est le score médian le plus haut pour l'objet suivi ; il peut donc différer du score affiché sur la miniature du résultat de recherche." + "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": { @@ -84,8 +88,8 @@ "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": "Saisir une nouvelle valeur de plaque d'immatriculation pour cet objet suivi" + "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", @@ -98,13 +102,26 @@ }, "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" } }, "type": { "details": "détails", "video": "vidéo", "object_lifecycle": "cycle de vie de l'objet", - "snapshot": "instantané" + "snapshot": "instantané", + "thumbnail": "Miniature", + "tracking_details": "Détails du suivi" }, "objectLifecycle": { "title": "Cycle de vie de l'objet", @@ -115,8 +132,8 @@ "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", + "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}}" @@ -124,7 +141,7 @@ "gone": "{{label}} parti", "heard": "{{label}} entendu", "external": "{{label}} détecté", - "active": "{{label}} est devenu actif", + "active": "{{label}} est devenu actif.", "header": { "zones": "Zones", "area": "Aire", @@ -134,7 +151,7 @@ "annotationSettings": { "title": "Paramètres d'annotation", "showAllZones": { - "title": "Montrer toutes les zones", + "title": "Afficher toutes les zones", "desc": "Afficher systématiquement les zones sur les images quand des objets y sont entrés" }, "offset": { @@ -170,8 +187,8 @@ "label": "Visualiser le cycle de vie de l'objet" }, "viewInHistory": { - "label": "Afficher dans l'historique", - "aria": "Afficher dans l'historique" + "label": "Afficher dans la chronologie", + "aria": "Afficher dans la chronologie" }, "downloadVideo": { "label": "Télécharger la vidéo", @@ -183,12 +200,34 @@ }, "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" } }, "dialog": { "confirmDelete": { "title": "Confirmer la suppression", - "desc": "La suppression de cet objet suivi supprime l'instantané, les représentations numériques enregistrées et les entrées du cycle de vie de l'objet associé. Les images enregistrées de cet objet suivi dans la vue Historique NE seront PAS supprimées.

    Êtes-vous sûr de vouloir continuer ?" + "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 ?" } }, "noTrackedObjects": "Aucun objet suivi trouvé", @@ -203,7 +242,64 @@ "error": "Échec de la suppression de l'objet suivi : {{errorMessage}}" } }, - "tooltip": "Correspondance : {{type}} à {{confidence}}%" + "tooltip": "Correspondance : {{type}} à {{confidence}}%", + "previousTrackedObject": "Objet suivi précédent", + "nextTrackedObject": "Objet suivi suivant" }, - "exploreMore": "Explorer plus d'objets {{label}}" + "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" + } + }, + "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 index ff8275a50..3b698d003 100644 --- a/web/public/locales/fr/views/exports.json +++ b/web/public/locales/fr/views/exports.json @@ -1,17 +1,23 @@ { - "documentTitle": "Exporter - Frigate", + "documentTitle": "Exports - Frigate", "search": "Rechercher", - "noExports": "Aucun export trouvé", - "deleteExport": "Supprimer l'export", - "deleteExport.desc": "Êtes-vous sûr de vouloir supprimer {{exportName}}?", + "noExports": "Aucune exportation trouvée", + "deleteExport": "Supprimer l'exportation", + "deleteExport.desc": "Êtes-vous sûr de vouloir supprimer {{exportName}} ?", "editExport": { - "title": "Renommer l'export", - "desc": "Saisissez un nouveau nom pour cet export.", - "saveExport": "Enregistrer l'export" + "title": "Renommer l'exportation", + "desc": "Saisissez un nouveau nom pour cette exportation.", + "saveExport": "Enregistrer l'exportation" }, "toast": { "error": { - "renameExportFailed": "Échec du renommage de l'export : {{errorMessage}}" + "renameExportFailed": "Échec du renommage de l'exportation : {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Partager l'exportation", + "downloadVideo": "Télécharger la vidéo", + "editName": "Modifier le nom", + "deleteExport": "Supprimer l'exportation" } } diff --git a/web/public/locales/fr/views/faceLibrary.json b/web/public/locales/fr/views/faceLibrary.json index fa5de03b2..7d65a5e91 100644 --- a/web/public/locales/fr/views/faceLibrary.json +++ b/web/public/locales/fr/views/faceLibrary.json @@ -1,7 +1,7 @@ { "description": { - "addFace": "Guide pour ajouter une nouvelle collection à la bibliothèque de visages", - "placeholder": "Saisissez un nom pour cette collection", + "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." }, "details": { @@ -17,18 +17,19 @@ "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}}" + "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 Entraîner 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 ; évitez d'utiliser des images d'entraînement où les visages sont capturés de biais.
  • " + "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": "Entraîner", - "aria": "Sélectionner entraîner", - "empty": "Il n'y a pas de tentatives récentes de reconnaissance faciale" + "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": { @@ -41,13 +42,13 @@ }, "selectItem": "Sélectionner {{item}}", "deleteFaceLibrary": { - "title": "Supprimer un nom", - "desc": "Etes-vous certain de vouloir supprimer la collection {{name}} ? Cette action supprimera définitivement tous les visages associés." + "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 et déposez une image ici, ou cliquez pour sélectionner", - "maxSize": "Taille max : {{size}}MB", + "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." } @@ -58,30 +59,30 @@ "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.", + "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 mis à jour avec succès.", - "deletedFace_one": "{{count}} visage a été supprimé avec succès.", - "deletedFace_many": "{{count}} visages ont été supprimés avec succès.", - "deletedFace_other": "{{count}} visages ont été supprimés avec succès.", - "trainedFace": "Visage entraîné avec succès.", - "renamedFace": "Visage renommé avec succés en {{name}}" + "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}}" }, "error": { "uploadingImageFailed": "Échec du téléversement de l'image : {{errorMessage}}", "deleteFaceFailed": "Échec de la suppression : {{errorMessage}}", - "trainFailed": "Échec de l'entrainement : {{errorMessage}}", + "trainFailed": "Échec de l'entraînement : {{errorMessage}}", "updateFaceScoreFailed": "Échec de la mise à jour du score du visage : {{errorMessage}}", - "addFaceLibraryFailed": "Échec du nommage du visage : {{errorMessage}}", + "addFaceLibraryFailed": "Échec de l'attribution du nom au visage : {{errorMessage}}", "deleteNameFailed": "Échec de la suppression du nom : {{errorMessage}}", - "renameFaceFailed": "Échec du renommage du visage : {{errorMessage}}" + "renameFaceFailed": "Échec du changement de nom du visage : {{errorMessage}}" } }, "trainFaceAs": "Entraîner le visage comme :", - "trainFace": "Entraîner un visage", + "trainFace": "Entraîner le visage", "steps": { "uploadFace": "Téléverser une image de visage", - "faceName": "Entrer un nom pour le 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." @@ -89,7 +90,7 @@ }, "renameFace": { "title": "Renommer le visage", - "desc": "Saisissez un nouveau nom pour {{name}}" + "desc": "Saisissez un nouveau nom pour {{name}}." }, "collections": "Collections", "deleteFaceAttempts": { @@ -98,6 +99,6 @@ "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": "Pas de visage disponible", + "nofaces": "Aucun visage disponible", "pixels": "{{area}} pixels" } diff --git a/web/public/locales/fr/views/live.json b/web/public/locales/fr/views/live.json index 8c8603972..e3edc5a0f 100644 --- a/web/public/locales/fr/views/live.json +++ b/web/public/locales/fr/views/live.json @@ -1,6 +1,6 @@ { "documentTitle": "Direct - Frigate", - "lowBandwidthMode": "Mode faible bande-passante", + "lowBandwidthMode": "Mode bande passante faible", "documentTitle.withCamera": "{{camera}} - Direct - Frigate", "twoWayTalk": { "disable": "Désactiver la conversation bidirectionnelle", @@ -14,17 +14,17 @@ "move": { "clickMove": { "label": "Cliquez dans le cadre pour centrer la caméra", - "enable": "Activer le click pour déplacer", - "disable": "Désactiver le click pour déplacer" + "enable": "Activer le clic pour déplacer", + "disable": "Désactiver le clic pour déplacer" }, "left": { - "label": "Déplacer la caméra PTZ sur la gauche" + "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 sur la droite" + "label": "Déplacer la caméra PTZ vers la droite" }, "down": { "label": "Déplacer la caméra PTZ vers le bas" @@ -32,18 +32,26 @@ }, "zoom": { "in": { - "label": "Zoomer avant de la caméra PTZ" + "label": "Zoom avant sur la caméra PTZ" }, "out": { - "label": "Zoom arrière de la caméra PTZ" + "label": "Zoom arrière sur la caméra PTZ" } }, "frame": { "center": { - "label": "Cliquez dans le cadre pour centrer la caméra PTZ" + "label": "Cliquez dans le cadre pour centrer la caméra PTZ." } }, - "presets": "Paramètres prédéfinis pour les caméras 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", @@ -71,53 +79,56 @@ }, "manualRecording": { "playInBackground": { - "label": "Jouer en arrière plan", - "desc": "Activer cette option pour continuer à streamer lorsque le lecteur est masqué." + "label": "Jouer en arrière-plan", + "desc": "Activez cette option pour continuer à diffuser lorsque le lecteur est masqué." }, "showStats": { "label": "Afficher les statistiques", - "desc": "Activer cette option pour afficher les statistiques de flux en surimpression sur le flux de la caméra." + "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": "Echec du démarrage de l'enregistrement à la demande manuel.", + "failedToStart": "Échec du démarrage de l'enregistrement manuel à la demande", "end": "Terminer l'enregistrement à la demande", - "ended": "Enregistrement à la demande terminé.", - "failedToEnd": "Impossible de terminer l'enregistrement manuel à la demande.", - "started": "Enregistrement à la demande démarré.", + "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": "Enregistrement à la demande", - "tips": "Démarrez un événement manuel en fonction des paramètres de conservation d'enregistrement de cette caméra." + "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 streaming", + "streamingSettings": "Paramètres de diffusion", "notifications": "Notifications", "suspend": { "forTime": "Mettre en pause pour : " }, "stream": { "audio": { - "available": "Audio disponible pour ce flux", + "available": "L'audio est disponible pour ce flux", "tips": { "documentation": "Lire la documentation ", - "title": "L'audio doit être capté par votre caméra et configuré dans go2rtc pour ce flux." + "title": "L'audio doit provenir de votre caméra et être configuré dans go2rtc pour ce flux." }, - "unavailable": "Audio non disponible pour ce flux" + "unavailable": "L'audio n'est pas disponible pour ce flux" }, "twoWayTalk": { - "tips": "Votre périphérique doit supporter la fonctionnalité et WebRTC doit être configuré pour supporter la conversation bidirectionnelle.", + "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 faible bande passante à cause d'erreurs de cache ou de flux.", + "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": "Activer cette option pour continuer le streaming lorsque le lecteur est masqué.", - "label": "Jouer en arrière plan" + "tips": "Activez cette option pour continuer la diffusion lorsque le lecteur est masqué.", + "label": "Jouer en arrière-plan" }, - "title": "Flux" + "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", @@ -126,10 +137,11 @@ "audioDetection": "Détection audio", "autotracking": "Suivi automatique", "cameraEnabled": "Caméra activée", - "title": "Paramètres de {{camera}}" + "title": "Paramètres de {{camera}}", + "transcription": "Transcription audio" }, "history": { - "label": "Afficher l'historique de capture" + "label": "Afficher les vidéos archivées" }, "effectiveRetainMode": { "modes": { @@ -153,6 +165,25 @@ "group": { "label": "Modifier le groupe de caméras" }, - "exitEdit": "Quitter l'édition" + "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." + } + }, + "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/recording.json b/web/public/locales/fr/views/recording.json index f04812f4c..e1960a754 100644 --- a/web/public/locales/fr/views/recording.json +++ b/web/public/locales/fr/views/recording.json @@ -1,12 +1,12 @@ { - "export": "Exporter", + "export": "Exports", "calendar": "Calendrier", "filter": "Filtre", "filters": "Filtres", "toast": { "error": { - "noValidTimeSelected": "Pas de période valide sélectionnée", - "endTimeMustAfterStartTime": "L'heure de fin doit être après l'heure de début" + "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/search.json b/web/public/locales/fr/views/search.json index b656ab889..a9938bff7 100644 --- a/web/public/locales/fr/views/search.json +++ b/web/public/locales/fr/views/search.json @@ -1,7 +1,7 @@ { "savedSearches": "Recherches enregistrées", "search": "Rechercher", - "searchFor": "Chercher {{inputValue}}", + "searchFor": "Rechercher {{inputValue}}", "button": { "clear": "Effacer la recherche", "filterInformation": "Filtrer les informations", @@ -13,20 +13,21 @@ "filter": { "label": { "zones": "Zones", - "sub_labels": "Sous-libellés", + "sub_labels": "Sous-étiquettes", "search_type": "Type de recherche", - "time_range": "Plage de temps", - "labels": "Libellés", + "time_range": "Plage horaire", + "labels": "Étiquettes", "cameras": "Caméras", "after": "Après", "before": "Avant", - "min_speed": "Vitesse minimum", - "max_speed": "Vitesse maximum", + "min_speed": "Vitesse minimale", + "max_speed": "Vitesse maximale", "min_score": "Score minimum", - "recognized_license_plate": "Plaques d'immatriculation reconnues", - "has_clip": "Contient un clip", - "has_snapshot": "Contient un instantané", - "max_score": "Score maximum" + "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", @@ -34,11 +35,11 @@ }, "toast": { "error": { - "beforeDateBeLaterAfter": "La date de début « avant » doit être postérieure à la date « après ».", + "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_min » doit être inférieure ou égale à la « vitesse_max ».", + "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 »." } }, @@ -54,11 +55,11 @@ "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 pour la suggestion ou saisissez la vôtre.", + "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": "Filtres de dates (avant : et après :) utilisez le format {{DateFormat}}.", + "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 :" } } diff --git a/web/public/locales/fr/views/settings.json b/web/public/locales/fr/views/settings.json index 67bd50933..d44b0117a 100644 --- a/web/public/locales/fr/views/settings.json +++ b/web/public/locales/fr/views/settings.json @@ -4,25 +4,31 @@ "authentication": "Paramètres d'authentification - Frigate", "camera": "Paramètres des caméras - Frigate", "classification": "Paramètres de classification - Frigate", - "motionTuner": "Réglages du mouvement - Frigate", - "general": "Paramètres généraux - Frigate", + "motionTuner": "Réglage de la détection de mouvement - Frigate", + "general": "Paramètres de l'interface utilisateur - 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 de données augmentées - Frigate" + "enrichments": "Paramètres d'enrichissements - Frigate", + "cameraManagement": "Gestion des caméras - Frigate", + "cameraReview": "Paramètres des activités caméra - Frigate" }, "menu": { "ui": "Interface utilisateur", "classification": "Classification", "masksAndZones": "Masques / Zones", - "motionTuner": "Réglages du mouvement", + "motionTuner": "Réglage de la détection de mouvement", "debug": "Débogage", "cameras": "Paramètres des caméras", "users": "Utilisateurs", "notifications": "Notifications", "frigateplus": "Frigate+", - "enrichments": "Données augmentées" + "enrichments": "Enrichissements", + "triggers": "Déclencheurs", + "roles": "Rôles", + "cameraManagement": "Gestion", + "cameraReview": "Activités" }, "dialog": { "unsavedChanges": { @@ -35,7 +41,7 @@ "noCamera": "Aucune caméra" }, "general": { - "title": "Paramètres généraux", + "title": "Paramètres de l'interface utilisateur", "liveDashboard": { "title": "Tableau de bord en direct", "automaticLiveView": { @@ -43,8 +49,16 @@ "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'alertes", + "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": { @@ -61,13 +75,13 @@ "title": "Visionneuse d'enregistrements", "defaultPlaybackRate": { "label": "Vitesse de lecture par défaut", - "desc": "Vitesse de lecture par défaut pour la lecture des enregistrements." + "desc": "Vitesse de lecture par défaut pour la lecture des enregistrements" } }, "calendar": { "firstWeekday": { "label": "Premier jour de la semaine", - "desc": "Le jour du début de semaine du calendrier de la revue d'évènements.", + "desc": "Jour du début de la semaine du calendrier des activités", "sunday": "Dimanche", "monday": "Lundi" }, @@ -126,7 +140,7 @@ }, "cameras": { "title": "Caméras", - "noCameras": "Aucune caméra disponible", + "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", @@ -136,12 +150,12 @@ "registerDevice": "Enregistrer cet appareil", "unregisterDevice": "Désenregistrer cet appareil", "sendTestNotification": "Envoyer une notification de test", - "unsavedChanges": "Modifications des notifications non enregistrés", + "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 non validée", + "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+.", @@ -150,12 +164,12 @@ "title": "Paramètres Frigate+", "snapshotConfig": { "documentation": "Lire la documentation", - "desc": "La soumission à Frigate+ nécessite que les instantanés et les instantanés clean_copy soient activés dans votre configuration.", - "title": "Configuration de l'instantané", + "desc": "La soumission à Frigate+ nécessite à la fois que les instantanés et les instantanés clean_copy soient activés dans votre configuration.", + "title": "Configuration des instantanés", "table": { "snapshots": "Instantanés", "camera": "Caméra", - "cleanCopySnapshots": "clean_copy Instantanés" + "cleanCopySnapshots": "Instantanés clean_copy" }, "cleanCopyWarning": "Certaines caméras ont des instantanés activés, mais la copie propre est désactivée. Vous devez activer clean_copy dans votre configuration d'instantanés pour pouvoir envoyer les images de ces caméras à Frigate+." }, @@ -166,7 +180,7 @@ "supportedDetectors": "Détecteurs pris en charge", "loading": "Chargement des informations du modèle…", "title": "Informations sur le modèle", - "trainDate": "Date d'entrainement", + "trainDate": "Date d'entraînement", "error": "Échec du chargement des informations du modèle", "availableModels": "Modèles disponibles", "dimensions": "Dimensions", @@ -182,7 +196,7 @@ "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és" + "unsavedChanges": "Modifications de paramètres de Frigate+ non enregistrées" }, "classification": { "title": "Paramètres de classification", @@ -278,6 +292,44 @@ "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": { @@ -288,7 +340,8 @@ "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." + "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": { @@ -312,7 +365,7 @@ }, "snapPoints": { "true": "Points d'accrochage", - "false": "Ne cassez pas les points" + "false": "Ne pas réunir les points" } }, "loiteringTime": { @@ -327,7 +380,7 @@ }, "speed": { "error": { - "mustBeGreaterOrEqualTo": "Le seuil de vitesse soit être égal ou supérieur à 0.1." + "mustBeGreaterOrEqualTo": "Le seuil de vitesse doit être supérieur ou égal à 0.1." } } }, @@ -341,12 +394,12 @@ "edit": "Modifier une zone", "name": { "title": "Nom", - "inputPlaceHolder": "Entrer un nom…", - "tips": "Le nom doit comporter au moins 2 caractères et ne doit pas être le nom d'une caméra ou d'une autre zone." + "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 latence" + "title": "Temps de maraudage" }, "speedEstimation": { "title": "Estimation de la vitesse", @@ -372,11 +425,11 @@ "point_other": "{{count}} points", "label": "Zones", "inertia": { - "desc": "Spécifie le nombre d'images qu'un objet doit avoir dans une zone avant d'être considéré comme faisant partie de la zone. Par défaut : 3", + "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. Redémarrez Frigate pour appliquer les modifications." + "success": "La zone ({{zoneName}}) a été enregistrée." }, "objects": { "title": "Objets", @@ -387,7 +440,7 @@ }, "motionMasks": { "label": "Masque de mouvement", - "documentTitle": "Modifier masque de mouvement - Frigate", + "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." @@ -404,8 +457,8 @@ "clickDrawPolygon": "Cliquer pour dessiner un polygone sur l'image.", "toast": { "success": { - "title": "{{polygonName}} a été enregistré. Redémarrez Frigate pour appliquer les modifications.", - "noName": "Le masque de mouvement a été enregistré. Redémarrez Frigate pour appliquer les modifications." + "title": "{{polygonName}} a été enregistré.", + "noName": "Le masque de mouvement a été enregistré." } }, "desc": { @@ -415,7 +468,7 @@ "add": "Nouveau masque de mouvement" }, "objectMasks": { - "label": "Masques de l'objet", + "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." @@ -429,15 +482,15 @@ }, "toast": { "success": { - "noName": "Le masque d'objet a été enregistré. Redémarrez Frigate pour appliquer les modifications.", - "title": "{{polygonName}} a été enregistré. Redémarrez Frigate pour appliquer les modifications." + "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 de l'objet - Frigate", + "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": { @@ -459,7 +512,7 @@ "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'objet." + "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", @@ -482,12 +535,12 @@ "debugging": "Débogage", "objectList": "Liste d'objets", "boundingBoxes": { - "title": "Cadres de délimitation", + "title": "Cadres de détection", "colors": { - "label": "Couleurs du cadre de délimitation d'un objet", - "info": "
  • Au démarrage, différentes couleurs seront attribuées à chaque libellé d'objet
  • Une fine ligne bleu foncé indique que l'objet n'est pas détecté à ce moment précis
  • Une fine ligne grise indique que l'objet est détecté comme étant stationnaire
  • Une ligne épaisse indique que l'objet fait l'objet d'un suivi automatique (lorsqu'il est activé)
  • " + "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élimitation autour des objets suivis" + "desc": "Afficher les cadres de détection autour des objets suivis" }, "timestamp": { "title": "Horodatage", @@ -504,11 +557,11 @@ "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ées sur les zones de l'image où un mouvement est actuellement détecté

    " + "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 une boîte de la région d'intérêt envoyée au détecteur d'objet", + "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": { @@ -523,7 +576,20 @@ "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." + "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", @@ -532,7 +598,7 @@ "desc": "Gérez les comptes utilisateurs de cette instance Frigate." }, "addUser": "Ajouter un utilisateur", - "updatePassword": "Mettre à jour le mot de passe", + "updatePassword": "Réinitialiser le mot de passe", "toast": { "success": { "roleUpdated": "Rôle mis à jour pour {{user}}", @@ -552,7 +618,7 @@ "actions": "Actions", "noUsers": "Aucun utilisateur trouvé.", "changeRole": "Changer le rôle d'utilisateur", - "password": "Mot de passe", + "password": "Réinitialiser le mot de passe", "deleteUser": "Supprimer un utilisateur", "role": "Rôle" }, @@ -560,35 +626,48 @@ "form": { "user": { "title": "Nom d'utilisateur", - "placeholder": "Entrez le 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": "Sécurité du mot de passe : ", + "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": "Entrez le mot de passe", + "notMatch": "Les mots de passe ne correspondent pas.", + "placeholder": "Saisir le mot de passe", "title": "Mot de passe", "confirm": { - "title": "Confirmez le mot de passe", - "placeholder": "Confirmez le mot de passe" + "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 8 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": "Entrez le nouveau mot de passe", + "placeholder": "Saisissez le nouveau mot de passe.", "confirm": { - "placeholder": "Ré-entrez le nouveau mot de passe" + "placeholder": "Confirmez le nouveau mot de passe." } }, - "usernameIsRequired": "Le nom d'utilisateur est requis", - "passwordIsRequired": "Mot de passe requis" + "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", @@ -597,10 +676,15 @@ }, "passwordSetting": { "updatePassword": "Mettre à jour le mot de passe pour {{username}}", - "setPassword": "Définir le mot de passe", + "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" + "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", @@ -610,42 +694,43 @@ "admin": "Administrateur", "adminDesc": "Accès complet à l'ensemble des fonctionnalités.", "viewer": "Observateur", - "viewerDesc": "Limité aux tableaux de bord Direct, Revue d'événements, Explorer et Exports." + "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, . ou _", + "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 des données augmentées", + "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-libellé. Cette information est intégréesà l'interface utilisateur, aux filtres de recherche et aux notifications." + "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 représentations numériques 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": "Confirmez la réindexation", + "desc": "La réindexation va régénérer les embeddings 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 représentations numériques 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." + "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 représentations numériques de recherche sémantique.", + "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 représentations numériques." + "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", @@ -653,35 +738,577 @@ }, "label": "Taille du modèle" }, - "desc": "La recherche sémantique de Frigate vous permet de retrouver les objets suivis dans votre revue d'évènements 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." + "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 des données augmentées", + "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.", + "desc": "La taille du modèle utilisé pour la reconnaissance faciale", "small": { "title": "petit", - "desc": "Utiliser petit emploie un modèle de représentation numérique faciale FaceNet qui s'exécute efficacement sur la plupart des processeurs." + "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 de représentation numérique faciale ArcFace et s'exécutera automatiquement sur le GPU si disponible." + "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-libellé à l'événement. Ces informations sont ensuite intégrées dans l'interface utilisateur, les options de filtrage et les notifications." + "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-libellé 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." + "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 de données augmentées ont été enregistrés. Redémarrez Frigate pour appliquer les modifications." + "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 des données augmentées modifiés)" + "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." + }, + "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" + } + } + }, + "cameraReview": { + "title": "Paramètres des activités caméra", + "object_descriptions": { + "title": "Descriptions d'objets par l'IA générative", + "desc": "Active ou désactive temporairement les descriptions d'objets générées par l'IA générative pour cette caméra. Lorsque cette option est désactivée, aucune description par l'IA n'est générée pour les objets suivis sur cette caméra." + }, + "review_descriptions": { + "title": "Descriptions des activités par l'IA générative", + "desc": "Active ou désactive temporairement les descriptions par l'IA générative pour cette caméra. Lorsque cette option est désactivée, aucune description nouvelle n'est générée pour 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." + } + } } } diff --git a/web/public/locales/fr/views/system.json b/web/public/locales/fr/views/system.json index 9b3d8a5dc..fde436292 100644 --- a/web/public/locales/fr/views/system.json +++ b/web/public/locales/fr/views/system.json @@ -3,7 +3,7 @@ "storage": "Statistiques de stockage - Frigate", "cameras": "Statistiques des caméras - Frigate", "general": "Statistiques générales - Frigate", - "enrichments": "Statistiques de données augmentées - Frigate", + "enrichments": "Statistiques d'enrichissements - Frigate", "logs": { "frigate": "Journaux de Frigate - Frigate", "nginx": "Journaux Nginx - Frigate", @@ -18,8 +18,8 @@ }, "copy": { "label": "Copier dans le presse-papiers", - "success": "Journaux copiés vers le presse-papiers", - "error": "Échec du copiage des journaux 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", @@ -27,7 +27,7 @@ "tag": "Balise", "message": "Message" }, - "tips": "Les logs sont diffusés en continu depuis le serveur", + "tips": "Les journaux sont diffusés en continu depuis le serveur", "toast": { "error": { "fetchingLogsFailed": "Erreur lors de la récupération des logs : {{errorMessage}}", @@ -40,22 +40,23 @@ "detector": { "title": "Détecteurs", "inferenceSpeed": "Vitesse d'inférence du détecteur", - "cpuUsage": "Utilisation processeur du détecteur", + "cpuUsage": "Utilisation CPU du détecteur", "memoryUsage": "Utilisation mémoire du détecteur", - "temperature": "Température 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": "Info matériel", - "gpuUsage": "Utilisation carte graphique", - "gpuMemory": "Mémoire carte graphique", - "gpuEncoder": "Encodeur carte graphique", - "gpuDecoder": "Décodeur carte graphique", + "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": "Tâche de sortie :", - "processError": "Erreur de tâche :" + "processOutput": "Sortie du processus :", + "processError": "Erreur du processus :" }, "nvidiaSMIOutput": { "title": "Sortie Nvidia SMI", @@ -65,22 +66,31 @@ "driver": "Pilote : {{driver}}" }, "copyInfo": { - "label": "Information de copie du GPU" + "label": "Copier les informations du GPU" }, "toast": { - "success": "Informations GPU copiées dans le presse-papier" + "success": "Informations GPU copiées dans le presse-papiers" }, "closeInfo": { - "label": "Information de fermeture du GPU" + "label": "Fermer les informations du GPU" } }, "npuUsage": "Utilisation NPU", - "npuMemory": "Mémoire 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." + } }, "otherProcesses": { - "title": "Autres tâches", - "processCpuUsage": "Utilisation processeur des tâches", - "processMemoryUsage": "Utilisation mémoire des tâches" + "title": "Autres processus", + "processCpuUsage": "Utilisation CPU du processus", + "processMemoryUsage": "Utilisation mémoire du processus", + "series": { + "go2rtc": "go2rtc", + "recording": "enregistrement" + } } }, "storage": { @@ -88,93 +98,108 @@ "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 sur votre disque." + "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 ne représente peut-être pas précisément l'espace libre et utilisable par Frigate si vous avez d'autres fichiers stockés sur ce disque en plus des enregistrements Frigate. Frigate ne suit pas l'utilisation du stockage en dehors de ses propres enregistrements." + "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": "Information sur le stockage non utilisé" + "unusedStorageInformation": "Informations sur le stockage non utilisé" }, - "overview": "Vue d'ensemble" + "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." + } }, "cameras": { "title": "Caméras", "info": { - "cameraProbeInfo": "{{camera}} Information récupérée depuis la caméra", - "fetching": "En cours de récupération des données de la caméra", + "cameraProbeInfo": "Informations de la sonde pour {{camera}}", + "fetching": "Récupération des données de la caméra en cours", "stream": "Flux {{idx}}", - "fps": "Images par seconde :", + "fps": "IPS :", "unknown": "Inconnu", "audio": "Audio :", "tips": { - "title": "Information récupérée depuis la caméra" + "title": "Informations de la sonde caméra" }, - "streamDataFromFFPROBE": "Le flux de données est obtenu par ffprobe.", + "streamDataFromFFPROBE": "Les données du flux sont obtenues avec ffprobe.", "resolution": "Résolution :", "error": "Erreur : {{error}}", "codec": "Codec :", "video": "Vidéo :", - "aspectRatio": "ratio d'aspect" + "aspectRatio": "rapport d'aspect" }, "framesAndDetections": "Images / Détections", "label": { "camera": "caméra", - "detect": "Détecter", - "skipped": "ignoré", + "detect": "détection", + "skipped": "ignorées", "ffmpeg": "FFmpeg", "capture": "capture", "cameraFfmpeg": "{{camName}} FFmpeg", - "cameraSkippedDetectionsPerSecond": "{{camName}} détections manquées par seconde", + "cameraSkippedDetectionsPerSecond": "{{camName}} détections ignorées par seconde", "overallDetectionsPerSecond": "Moyenne de détections par seconde", - "overallFramesPerSecond": "Moyenne d'images par seconde", - "overallSkippedDetectionsPerSecond": "Moyenne de détections manquées par seconde", + "overallFramesPerSecond": "Moyenne d'images par seconde (IPS)", + "overallSkippedDetectionsPerSecond": "Moyenne de détections ignorées par seconde", "cameraCapture": "{{camName}} capture", "cameraDetect": "{{camName}} détection", - "cameraFramesPerSecond": "{{camName}} images par seconde", + "cameraFramesPerSecond": "{{camName}} images par seconde (IPS)", "cameraDetectionsPerSecond": "{{camName}} détections par seconde" }, "overview": "Vue d'ensemble", "toast": { "success": { - "copyToClipboard": "Données récupérées copiées dans le presse-papier." + "copyToClipboard": "Données de la sonde copiées dans le presse-papiers" }, "error": { - "unableToProbeCamera": "Impossible de récupérer des infos depuis la caméra : {{errorMessage}}" + "unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}" } } }, "lastRefreshed": "Dernier rafraichissement : ", "stats": { "ffmpegHighCpuUsage": "{{camera}} a un taux élevé d'utilisation processeur par FFmpeg ({{ffmpegAvg}}%)", - "detectHighCpuUsage": "{{camera}} a un taux élevé d'utilisation processeur ({{detectAvg}}%)", + "detectHighCpuUsage": "{{camera}} : charge CPU détection élevée ({{detectAvg}}%)", "healthy": "Le système est sain", - "reindexingEmbeddings": "Réindexation des représentations numériques ({{processed}}% complété)", + "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)" + "detectIsVerySlow": "{{detect}} est très lent ({{speed}} ms)", + "shmTooLow": "L'allocation /dev/shm ({{total}} Mo) devrait être augmentée à au moins {{min}} Mo." }, "enrichments": { - "title": "Données augmentées", + "title": "Enrichissements", "infPerSecond": "Inférences par seconde", "embeddings": { - "face_embedding_speed": "Vitesse de capture des données complémentaires de visage", - "text_embedding_speed": "Vitesse de capture des données complémentaire de texte", - "image_embedding_speed": "Vitesse de capture des données complémentaires à l'image", + "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": "Représentations numériques d'image", + "image_embedding": "Embedding d'image", "yolov9_plate_detection": "Détection de plaques d'immatriculation YOLOv9", "face_recognition": "Reconnaissance faciale", - "text_embedding": "Représentation numérique de texte", - "yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9" - } + "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/common.json b/web/public/locales/gl/common.json index 1443cce35..61b9ce58f 100644 --- a/web/public/locales/gl/common.json +++ b/web/public/locales/gl/common.json @@ -9,5 +9,6 @@ "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 index 2a0bee0d5..8b0857dac 100644 --- a/web/public/locales/gl/components/auth.json +++ b/web/public/locales/gl/components/auth.json @@ -5,7 +5,8 @@ "errors": { "passwordRequired": "Contrasinal obrigatorio", "unknownError": "Erro descoñecido. Revisa os logs.", - "usernameRequired": "Usuario/a obrigatorio" + "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/dialog.json b/web/public/locales/gl/components/dialog.json index c6519972a..d2aff40d1 100644 --- a/web/public/locales/gl/components/dialog.json +++ b/web/public/locales/gl/components/dialog.json @@ -15,6 +15,9 @@ "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 index 6927e2e51..8ef5f8fd1 100644 --- a/web/public/locales/gl/components/filter.json +++ b/web/public/locales/gl/components/filter.json @@ -6,7 +6,8 @@ "all": { "short": "Etiquetas", "title": "Todas as Etiquetas" - } + }, + "count_other": "{{count}} Etiquetas" }, "zones": { "all": { diff --git a/web/public/locales/gl/views/classificationModel.json b/web/public/locales/gl/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/gl/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/gl/views/events.json b/web/public/locales/gl/views/events.json index c5c9cb67b..56da9d9e5 100644 --- a/web/public/locales/gl/views/events.json +++ b/web/public/locales/gl/views/events.json @@ -6,5 +6,8 @@ "motion": { "only": "Só movemento", "label": "Movemento" + }, + "empty": { + "alert": "Non hai alertas que revisar" } } diff --git a/web/public/locales/he/audio.json b/web/public/locales/he/audio.json index f7369853c..711a8d338 100644 --- a/web/public/locales/he/audio.json +++ b/web/public/locales/he/audio.json @@ -18,7 +18,7 @@ "humming": "זמזום", "groan": "אנקה", "grunt": "לנחור", - "whistling": "שריקה", + "whistling": "לשרוק", "breathing": "נשימה", "wheeze": "גניחה", "snoring": "נחירה", @@ -69,7 +69,7 @@ "fly": "זבוב", "buzz": "זמזם.", "frog": "צפרדע", - "croak": "קרקור.", + "croak": "קִרקוּר", "snake": "נחש", "rattle": "טרטור", "whale_vocalization": "קולות לוויתן", @@ -81,7 +81,7 @@ "bass_guitar": "גיטרה בס", "acoustic_guitar": "גיטרה אקוסטית", "steel_guitar": "גיטרה פלדה", - "tapping": "הקשה.", + "tapping": "להקיש", "strum": "פריטה", "banjo": "בנג'ו", "sitar": "סיטאר", @@ -94,7 +94,7 @@ "electronic_organ": "אורגן חשמלי", "hammond_organ": "עוגב המונד", "synthesizer": "סינתיסייזר", - "sampler": "דגם", + "sampler": "דוגם", "harpsichord": "צֶ'מבָּלוֹ", "percussion": "הַקָשָׁה", "boat": "סירה", @@ -102,7 +102,7 @@ "motorcycle": "אופנוע", "bus": "אוטובוס", "bicycle": "אופניים", - "train": "למד פנים", + "train": "אימון", "skateboard": "סקייטבורד", "camera": "מצלמה", "howl": "יללה", @@ -189,7 +189,7 @@ "church_bell": "פעמון כנסיה", "jingle_bell": "ג'ינגל בל", "bicycle_bell": "פעמון אופניים", - "chime": "צלצול", + "chime": "צִלצוּל", "wind_chime": "פעמון רוח", "harmonica": "הרמוניקה", "accordion": "אקורדיון", @@ -341,7 +341,7 @@ "microwave_oven": "מיקרוגל", "water_tap": "ברז מים", "bathtub": "אמבטיה", - "dishes": "כלים.", + "dishes": "מנות", "scissors": "מספריים", "toothbrush": "מברשת שיניים", "toilet_flush": "הורדת מים לאסלה", @@ -355,7 +355,7 @@ "computer_keyboard": "מקלדת מחשב", "writing": "כתיבה", "telephone_bell_ringing": "צלצול טלפון", - "ringtone": "צליל חיוג.", + "ringtone": "צלצול", "clock": "שעון", "telephone_dialing": "טלפון מחייג", "dial_tone": "צליל חיוג", @@ -425,5 +425,79 @@ "slam": "טריקה", "telephone": "טלפון", "tuning_fork": "מזלג כוונון", - "raindrop": "טיפות גשם" + "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 index e6c1d632f..1059ae300 100644 --- a/web/public/locales/he/common.json +++ b/web/public/locales/he/common.json @@ -78,7 +78,10 @@ "12hour": "MMM d, yyyy" }, "30minutes": "30 דקות", - "thisMonth": "החודש" + "thisMonth": "החודש", + "inProgress": "בתהליך", + "invalidStartTime": "זמן התחלה לא תקין", + "invalidEndTime": "זמן סיום לא תקין" }, "unit": { "speed": { @@ -88,10 +91,24 @@ "length": { "feet": "רגל", "meters": "מטרים" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" } }, "label": { - "back": "אחורה" + "back": "אחורה", + "hide": "הסתר {{item}}", + "show": "הצג {{item}}", + "ID": "ID", + "none": "ללא", + "all": "הכל", + "other": "אחר" }, "button": { "apply": "החל", @@ -128,7 +145,8 @@ "on": "פעיל", "download": "הורדה", "info": "מידע", - "next": "הבא" + "next": "הבא", + "continue": "המשך" }, "menu": { "system": "מערכת", @@ -172,9 +190,17 @@ "ja": "יפנית", "de": "גרמנית", "yue": "קנטונזית", - "ca": "קטלה (קטלאנית)" + "ca": "קטלה (קטלאנית)", + "ptBR": "פורטוגזית - ברזיל", + "sr": "סרבית", + "sl": "סלובנית", + "lt": "ליטאית", + "bg": "בולגרית", + "gl": "Galego", + "id": "אינדונזית", + "ur": "اردو" }, - "appearance": "מראה.", + "appearance": "מראה", "darkMode": { "label": "מצב כהה", "light": "בהיר", @@ -221,7 +247,8 @@ "current": "משתמש מחובר: {{user}}", "setPassword": "קביעת סיסמה", "title": "משתמש" - } + }, + "classification": "סיווג" }, "toast": { "copyUrlToClipboard": "כתובת האתר המועתקת.", @@ -261,5 +288,18 @@ "title": "404", "desc": "דף לא נמצא" }, - "selectItem": "בחירה:{{item}}" + "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 index 17b28cba1..0f6caf3cf 100644 --- a/web/public/locales/he/components/auth.json +++ b/web/public/locales/he/components/auth.json @@ -10,6 +10,7 @@ "webUnknownError": "שגיאה לא ידועה, בדוק את הלוגים.", "rateLimit": "חרגת מהמגבלת בקשות. נסה שוב מאוחר יותר.", "loginFailed": "ההתחברות נכשלה" - } + }, + "firstTimeLogin": "מתחבר בפעם הראשונה? פרטי ההתחברות מודפסים בלוגים של פריגייט." } } diff --git a/web/public/locales/he/components/camera.json b/web/public/locales/he/components/camera.json index f9de9a6c1..184b192bd 100644 --- a/web/public/locales/he/components/camera.json +++ b/web/public/locales/he/components/camera.json @@ -41,7 +41,8 @@ "label": "מצב תאימות", "desc": "הפעל אפשרות זו רק אם השידור החי של המצלמה שלך מציג עיוותים בצבע ויש לו קו אלכסוני בצד ימין של התמונה." } - } + }, + "birdseye": "מבט על" }, "edit": "ערכית קבוצת מצלמות", "delete": { diff --git a/web/public/locales/he/components/dialog.json b/web/public/locales/he/components/dialog.json index 472d3d541..85353b993 100644 --- a/web/public/locales/he/components/dialog.json +++ b/web/public/locales/he/components/dialog.json @@ -15,7 +15,8 @@ "failed": "נכשל בהתחלת הייצוא: {{error}}", "noVaildTimeSelected": "לא נבחר טווח זמן תקף" }, - "success": "הייצוא הוחל בהצלחה. הצג את הקובץ בתיקייה /ייצוא." + "success": "הייצוא התחיל בהצלחה. ניתן לצפות בקובץ בעמוד הייצוא.", + "view": "תצוגה" }, "time": { "end": { @@ -108,7 +109,16 @@ "button": { "export": "ייצוא", "markAsReviewed": "סמן כסוקר", - "deleteNow": "מחיקה כעת" + "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 index 2316722ca..b11aba954 100644 --- a/web/public/locales/he/components/filter.json +++ b/web/public/locales/he/components/filter.json @@ -1,11 +1,11 @@ { - "filter": "לסנן", + "filter": "מסנן", "features": { "submittedToFrigatePlus": { "tips": "עליך תחילה לסנן לפי אובייקטים במעקב שיש להם תמונת מצב.

    לא ניתן לשלוח ל-Frigate+ אובייקטים במעקב ללא תמונת מצב.", "label": "העלאה ל- +Frigate" }, - "label": "מאפיינים", + "label": "תכונות", "hasVideoClip": "קיים סרטון", "hasSnapshot": "קיימת לכידת תמונה" }, @@ -26,7 +26,7 @@ } }, "dates": { - "selectPreset": "בחר פריסט…", + "selectPreset": "בחר הגדרה…", "all": { "title": "כל התאריכים", "short": "תאריכים" @@ -71,16 +71,16 @@ "title": "הגדרות", "defaultView": { "summary": "סיכום", - "unfilteredGrid": "תצוגה מלאה", + "unfilteredGrid": "טבלה לא מסוננת", "title": "תצוגת ברירת מחדל", "desc": "כאשר לא נבחרו מסננים, הצג סיכום של האובייקטים האחרונים שעברו מעקב לפי תווית, או הצג רשת לא מסוננת." }, "gridColumns": { - "title": "עמודות גריד", - "desc": "בחר את מספר העמודות בגריד." + "title": "עמודות טבלה", + "desc": "בחר את מספר העמודות בטבלה." }, "searchSource": { - "label": "מקור חיפוש", + "label": "חיפוש במקור", "desc": "בחר אם לחפש בתמונות הממוזערות או בתיאורים של האובייקטים שבמעקב.", "options": { "thumbnailImage": "תמונה ממוזערת", @@ -100,7 +100,7 @@ "error": "מחיקת אובייקטים במעקב נכשלה: {{errorMessage}}" }, "title": "אישור מחיקה", - "desc": "מחיקת אובייקטים אלה שעברו מעקב ({{objectLength}}) מסירה את לכידת התמונה, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקטים אלה שעברו מעקב בתצוגת היסטוריה לא יימחקו.

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

    החזק את מקש Shift כדי לעקוף תיבת דו-שיח זו בעתיד." + "desc": "מחיקת אובייקטים אלה ({{objectLength}}) שעברו מעקב מסירה את לכידת התמונה, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקטים אלה שעברו מעקב בתצוגת היסטוריה לא יימחקו.

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

    החזק את מקש Shift כדי לעקוף תיבת דו-שיח זו בעתיד." }, "zoneMask": { "filterBy": "סינון לפי מיסוך אזור" @@ -111,16 +111,30 @@ "loading": "טוען לוחיות רישוי מזוהות…", "placeholder": "הקלד כדי לחפש לוחיות רישוי…", "noLicensePlatesFound": "לא נמצאו לוחיות רישוי.", - "selectPlatesFromList": "בחירת לוחית אחת או יותר מהרשימה." + "selectPlatesFromList": "בחירת לוחית אחת או יותר מהרשימה.", + "selectAll": "בחר הכל", + "clearAll": "נקה הכל" }, "logSettings": { "label": "סינון רמת לוג", - "filterBySeverity": "סנן לוגים לפי חומרה", + "filterBySeverity": "סנן לוגים לפי חוּמרָה", "loading": { "title": "טוען", - "desc": "כאשר חלונית הלוגים גוללת לתחתית, לוגים חדשים מוזרמים אוטומטית עם הוספתם." + "desc": "כאשר חלונית הלוגים מגוללת לתחתית, לוגים חדשים מוזרמים אוטומטית עם הוספתם." }, "disableLogStreaming": "השבתת זרימה של לוגים", "allLogs": "כל הלוגים" + }, + "classes": { + "label": "מחלקות", + "all": { + "title": "כל המחלקות" + }, + "count_one": "{{count}} מחלקה", + "count_other": "{{count}} מחלקות" + }, + "attributes": { + "label": "מאפייני סיווג", + "all": "כל המאפיינים" } } diff --git a/web/public/locales/he/objects.json b/web/public/locales/he/objects.json index 68f648da0..65f00b1b7 100644 --- a/web/public/locales/he/objects.json +++ b/web/public/locales/he/objects.json @@ -5,7 +5,7 @@ "motorcycle": "אופנוע", "airplane": "מטוס", "bus": "אוטובוס", - "train": "למד פנים", + "train": "אימון", "boat": "סירה", "traffic_light": "רמזור", "fire_hydrant": "ברז כיבוי אש", diff --git a/web/public/locales/he/views/classificationModel.json b/web/public/locales/he/views/classificationModel.json new file mode 100644 index 000000000..0e965eb74 --- /dev/null +++ b/web/public/locales/he/views/classificationModel.json @@ -0,0 +1,192 @@ +{ + "documentTitle": "מודלי סיווג - Frigate", + "details": { + "scoreInfo": "הציון מייצג את ממוצע רמת הביטחון של הסיווג, על פני כל הזיהויים של האובייקט הזה.", + "none": "ללא ערך", + "unknown": "לא ידוע" + }, + "button": { + "deleteClassificationAttempts": "מחיקת אוסף התמונות", + "renameCategory": "שינוי שם קטגוריה", + "deleteCategory": "מחיקת קטגוריה", + "deleteImages": "מחיקת תמונות", + "trainModel": "אימון מודל", + "addClassification": "הוספת סיווג", + "deleteModels": "מחיקת מודלים", + "editModel": "עריכת מודל" + }, + "tooltip": { + "trainingInProgress": "המודל נמצא כרגע בתהליך אימון", + "noNewImages": "אין תמונות חדשות לאימון. קודם סווג עוד תמונות במערך הנתונים (Dataset).", + "noChanges": "לא בוצעו שינויים במערך הנתונים מאז האימון האחרון.", + "modelNotReady": "המודל עדיין לא מוכן לאימון" + }, + "toast": { + "success": { + "deletedCategory": "הקטגוריה נמחקה", + "deletedImage": "התמונות נמחקו", + "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 index 7f9120a31..535619a34 100644 --- a/web/public/locales/he/views/configEditor.json +++ b/web/public/locales/he/views/configEditor.json @@ -1,5 +1,5 @@ { - "documentTitle": "עורך הגדרות - פריגטה", + "documentTitle": "עורך הגדרות - Frigate", "configEditor": "עורך תצורה", "copyConfig": "העתקת הגדרות", "saveAndRestart": "שמירה והפעלה מחדש", @@ -12,5 +12,7 @@ "error": { "savingError": "שגיאה בשמירת ההגדרות" } - } + }, + "safeConfigEditor": "עורך תצורה (מצב בטוח)", + "safeModeDescription": "Frigate במצב בטוח עקב שגיאת אימות הגדרות." } diff --git a/web/public/locales/he/views/events.json b/web/public/locales/he/views/events.json index 21b551e2a..636a073b1 100644 --- a/web/public/locales/he/views/events.json +++ b/web/public/locales/he/views/events.json @@ -9,7 +9,11 @@ "empty": { "detection": "אין גילויים לבדיקה", "alert": "אין התראות להצגה", - "motion": "לא נמצאו נתוני תנועה" + "motion": "לא נמצאו נתוני תנועה", + "recordingsDisabled": { + "title": "יש להפעיל הקלטות", + "description": "ניתן ליצור פריטי סקירה עבור מצלמה רק כאשר הקלטות מופעלות עבור אותה מצלמה." + } }, "timeline": "ציר זמן", "timeline.aria": "בחירת ציר זמן", @@ -34,5 +38,28 @@ "selected_one": "נבחרו {{count}}", "selected_other": "{{count}} נבחרו", "camera": "מצלמה", - "detected": "זוהה" + "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 index 0646e5089..6042b4329 100644 --- a/web/public/locales/he/views/explore.json +++ b/web/public/locales/he/views/explore.json @@ -27,6 +27,28 @@ }, "deleteTrackedObject": { "label": "מחק את אובייקט המעקב הזה" + }, + "audioTranscription": { + "aria": "בקשת תמלול אודיו", + "label": "תמלל" + }, + "showObjectDetails": { + "label": "הצגת מסלול האובייקט" + }, + "hideObjectDetails": { + "label": "הסתרת מסלול האובייקט" + }, + "downloadCleanSnapshot": { + "label": "הורד תמונה נקיה", + "aria": "הורד תמונה נקיה" + }, + "viewTrackingDetails": { + "label": "הצג פרטי מעקב", + "aria": "הצג את פרטי המעקב" + }, + "addTrigger": { + "label": "הוסף טריגר", + "aria": "הוסף טריגר לאובייקט במעקב זה" } }, "generativeAI": "Generative - AI", @@ -64,7 +86,9 @@ "details": "פרטים", "snapshot": "לכידת תמונה", "video": "וידיאו", - "object_lifecycle": "שלבי זיהוי של האובייקט" + "object_lifecycle": "שלבי זיהוי של האובייקט", + "thumbnail": "תמונה ממוזערת", + "tracking_details": "פרטי מעקב" }, "objectLifecycle": { "title": "שלבי זיהוי של האובייקט", @@ -132,12 +156,16 @@ "success": { "updatedSublabel": "תווית המשנה עודכנה בהצלחה.", "updatedLPR": "לוחית הרישוי עודכנה בהצלחה.", - "regenerate": "תיאור חדש התבקש מ-{{provider}}. בהתאם למהירות הספק שלך, ייתכן שייקח זמן מה ליצירת התיאור החדש." + "regenerate": "תיאור חדש התבקש מ-{{provider}}. בהתאם למהירות הספק שלך, ייתכן שייקח זמן מה ליצירת התיאור החדש.", + "updatedAttributes": "המאפיינים עודכנו בהצלחה.", + "audioTranscription": "בקשת תמלול האודיו נשלחה בהצלחה. בהתאם למהירות שרת ה־Frigate שלך, התמלול עשוי להימשך זמן מה עד להשלמתו." }, "error": { "regenerate": "ההתקשרות ל-{{provider}} לקבלת תיאור חדש נכשלה: {{errorMessage}}", "updatedSublabelFailed": "עדכון תווית המשנה נכשל: {{errorMessage}}", - "updatedLPRFailed": "עדכון לוחית הרישוי נכשל: {{errorMessage}}" + "updatedLPRFailed": "עדכון לוחית הרישוי נכשל: {{errorMessage}}", + "updatedAttributesFailed": "נכשל בעדכון המאפיינים: {{errorMessage}}", + "audioTranscription": "נכשל בשליחת בקשה לתמלול אודיו: {{errorMessage}}" } }, "title": "סקירת הפריט", @@ -184,12 +212,23 @@ "descriptionSaved": "התיאור נשמר בהצלחה", "saveDescriptionFailed": "עדכון התיאור נכשל: {{errorMessage}}" }, - "zones": "אזורים" + "zones": "אזורים", + "editAttributes": { + "title": "ערוך מאפיינים", + "desc": "בחר מאפייני סיווג עבור {{label}} זה" + }, + "score": { + "label": "ציון" + }, + "attributes": "מאפייני סיווג", + "title": { + "label": "כותרת" + } }, "dialog": { "confirmDelete": { "title": "אישור מחיקה", - "desc": "מחיקת אובייקט זה במעקב מסירה את תמונת המצב, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקט זה במעקב בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?" + "desc": "מחיקת אובייקט זה במעקב תסיר את הצילום, כל ה־embeddings השמורים וכל רשומות פרטי המעקב המשויכות. קטעי וידאו מוקלטים של אובייקט זה בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?" } }, "searchResult": { @@ -199,11 +238,68 @@ "error": "מחיקת האובייקט במעקב נכשלה: {{errorMessage}}", "success": "האובייקט המעקב נמחק בהצלחה." } - } + }, + "previousTrackedObject": "האובייקט הקודם במעקב", + "nextTrackedObject": "האובייקט הבא במעקב" }, "noTrackedObjects": "לא נמצאו אובייקטים במעקב", "fetchingTrackedObjectsFailed": "שגיאה באחזור אובייקטים במעקב: {{errorMessage}}", "trackedObjectsCount_one": "אובייקט במעקב ({{count}}) ", "trackedObjectsCount_two": "אובייקטים במעקב ({{count}}) ", - "trackedObjectsCount_other": "אובייקטים במעקב ({{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 index 93e26a7b8..2bd8c7e00 100644 --- a/web/public/locales/he/views/exports.json +++ b/web/public/locales/he/views/exports.json @@ -13,5 +13,11 @@ "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 index 96b248100..0918c847d 100644 --- a/web/public/locales/he/views/faceLibrary.json +++ b/web/public/locales/he/views/faceLibrary.json @@ -1,11 +1,11 @@ { "description": { - "addFace": "עיין בהוספת אוסף חדש לספריית הפנים.", + "addFace": "הוסף אוסף חדש לספריית הפנים באמצעות העלאת התמונה הראשונה שלך.", "placeholder": "הזנת שם לאוסף זה", - "invalidName": "שם לא חוקי. שמות יכולים לכלול רק אותיות, מספרים, רווחים, גרשים, קווים תחתונים ומקפים." + "invalidName": "שם לא תקין. שמות יכולים לכלול רק אותיות, מספרים, רווחים, גרש (’), קו תחתון (_) ומקף (-)." }, "createFaceLibrary": { - "nextSteps": "כדי לבנות בסיס חזק:
  • השתמשו בכרטיסייה 'אימון' כדי לבחור ולאמן תמונות עבור כל אדם שזוהה.
  • התמקדו בתמונות ישירות לקבלת התוצאות הטובות ביותר; הימנעו מאימון תמונות שלוכדות פנים בזווית.
  • ", + "nextSteps": "כדי לבנות בסיס חזק:
  • השתמש בלשונית זיהויים אחרונים כדי לבחור ולאמן על תמונות עבור כל אדם שזוהה.
  • כדי לקבל תוצאות מיטביות, התמקד בתמונות פנים מלפנים; הימנע מתמונות אימון שבהן הפנים מצולמות בזווית.
  • ", "title": "יצירת אוסף", "desc": "יצירת אוסף חדש", "new": "יצירת פנים חדשות" @@ -22,7 +22,7 @@ "addFaceLibrary": "{{name}} נוסף בהצלחה לספריית הפנים!", "renamedFace": "שם הפנים שונה בהצלחה ל-{{name}}", "trainedFace": "פנים אומנו בהצלחה.", - "updatedFaceScore": "ציון הפנים עודכן בהצלחה." + "updatedFaceScore": "ציון הפנים עבור {{name}} עודכן בהצלחה ({{score}})." }, "error": { "deleteFaceFailed": "המחיקה נכשלה: {{errorMessage}}", @@ -58,9 +58,10 @@ } }, "train": { - "title": "רכבת", - "aria": "בחירת אימון", - "empty": "אין ניסיונות זיהוי פנים אחרונים" + "title": "זיהויים אחרונים", + "aria": "בחירת זיהויים אחרונים", + "empty": "אין ניסיונות זיהוי פנים אחרונים", + "titleShort": "לאחרונה" }, "selectItem": "בחירה:{{item}}", "selectFace": "בחירת פנים", @@ -91,7 +92,7 @@ "selectImage": "בחירת קובץ תמונה." }, "dropActive": "שחרר/י את התמונה כאן…", - "dropInstructions": "גרור ושחרר תמונה כאן, או לחץ כדי לבחור", + "dropInstructions": "גרור ושחרר או הדבק תמונה כאן, או לחץ כדי לבחור", "maxSize": "גודל מקסימאלי: {{size}}MB" }, "nofaces": "אין פנים זמינים", diff --git a/web/public/locales/he/views/live.json b/web/public/locales/he/views/live.json index 9fccbd158..5426392b9 100644 --- a/web/public/locales/he/views/live.json +++ b/web/public/locales/he/views/live.json @@ -1,7 +1,7 @@ { "manualRecording": { - "title": "הקלטה לפי דרישה", - "tips": "התחלת אירוע הקלטה ידני המבוסס על הגדרות שמירת ההקלטה של מצלמה זו.", + "title": "לפי דרישה", + "tips": "הורד צילום מיידי או התחל אירוע ידני בהתאם להגדרות שמירת ההקלטות של מצלמה זו.", "playInBackground": { "label": "ניגון ברקע", "desc": "הפעל אפשרות זו כדי להמשיך להזרים גם כאשר הנגן מוסתר." @@ -63,7 +63,15 @@ "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה הממונעת" } }, - "presets": "מצלמה ממונעת - פריסטים" + "presets": "מצלמה ממונעת - פריסטים", + "focus": { + "in": { + "label": "כניסת פוקוס מצלמת PTZ" + }, + "out": { + "label": "יציאת פוקוס מצלמת PTZ" + } + } }, "camera": { "enable": "אפשור מצלמה", @@ -120,6 +128,9 @@ }, "available": "קול זמין עבור שידור זה", "unavailable": "קול אינו זמין עבור שידור זה" + }, + "debug": { + "picker": "בחירת זרם אינה זמינה במצב Debug. תצוגת Debug תמיד משתמשת בזרם שמוגדר עם הייעוד detect." } }, "cameraSettings": { @@ -129,7 +140,8 @@ "recording": "הקלטה", "snapshots": "לכידת תמונה", "audioDetection": "זיהוי קול", - "autotracking": "מעקב אוטומטי" + "autotracking": "מעקב אוטומטי", + "transcription": "תמלול אודיו" }, "streamingSettings": "הגדרות שידור", "notifications": "התראות", @@ -154,5 +166,24 @@ "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/recording.json b/web/public/locales/he/views/recording.json index 91817595b..1e45f6b6b 100644 --- a/web/public/locales/he/views/recording.json +++ b/web/public/locales/he/views/recording.json @@ -1,5 +1,5 @@ { - "filter": "לסנן", + "filter": "מסנן", "export": "ייצוא", "calendar": "לוח שנה", "filters": "מסננים", diff --git a/web/public/locales/he/views/search.json b/web/public/locales/he/views/search.json index 0865e4654..199171375 100644 --- a/web/public/locales/he/views/search.json +++ b/web/public/locales/he/views/search.json @@ -4,7 +4,7 @@ "searchFor": "חפש את{{inputValue}}", "button": { "clear": "ניקוי חיפוש", - "save": "שמירת החיפוש", + "save": "שמור חיפוש", "delete": "מחיקת חיפוש שמור", "filterInformation": "סינון מידע", "filterActive": "מסננים פעילים" @@ -26,7 +26,8 @@ "min_speed": "מהירות מינמאלית", "recognized_license_plate": "לוחית רישוי מוכרת", "has_clip": "קיים סרטון קליפ", - "has_snapshot": "לכידת תמונה קיימת" + "has_snapshot": "לכידת תמונה קיימת", + "attributes": "מאפיינים" }, "searchType": { "thumbnail": "תמונה ממוזערת", diff --git a/web/public/locales/he/views/settings.json b/web/public/locales/he/views/settings.json index a628048fc..cb24111c0 100644 --- a/web/public/locales/he/views/settings.json +++ b/web/public/locales/he/views/settings.json @@ -46,7 +46,8 @@ "mustBeAtLeastTwoCharacters": "שם האזור חייב להיות באורך של לפחות 2 תווים.", "mustNotBeSameWithCamera": "שם האזור לא חייב להיות זהה לשם המצלמה.", "mustNotContainPeriod": "שם האזור אינו יכול להכיל נקודות.", - "hasIllegalCharacter": "שם האזור מכיל תווים לא חוקיים." + "hasIllegalCharacter": "שם האזור מכיל תווים לא חוקיים.", + "mustHaveAtLeastOneLetter": "שם האזור חייב לכלול לפחות אות אחת." } }, "distance": { @@ -111,7 +112,7 @@ "name": { "title": "שם", "inputPlaceHolder": "הזן שם…", - "tips": "השם חייב להיות באורך של לפחות 2 תווים ואינו יכול להיות שם של מצלמה או אזור אחר." + "tips": "השם חייב להכיל לפחות 2 תווים, לכלול לפחות אות אחת, ואסור שיהיה זהה לשם של מצלמה או של אזור אחר במצלמה זו." }, "point_one": "נקודה {{count}}", "point_two": "נקודות {{count}}", @@ -125,7 +126,7 @@ "desc": "קובע את משך הזמן המינימלי בשניות שהאובייקט חייב להיות באזור כדי שיופעל. ברירת מחדל: 0" }, "objects": { - "title": "אובייקט", + "title": "אובייקטים", "desc": "רשימת אובייקטים החלים על אזור זה." }, "speedEstimation": { @@ -148,7 +149,7 @@ } }, "toast": { - "success": "האזור ({{zoneName}}) נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + "success": "האזור ({{zoneName}}) נשמר בהצלחה." }, "allObjects": "כל האובייקטים" }, @@ -176,8 +177,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים.", - "noName": "מיסוך התנועה נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + "title": "{{polygonName}} נשמר בהצלחה.", + "noName": "מסכת תנועה נשמרה בהצלחה." } } }, @@ -202,8 +203,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים.", - "noName": "מיסוך האובייקט נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + "title": "{{polygonName}} נשמר בהצלחה.", + "noName": "מיסוך האובייקט נשמר." } } } @@ -225,6 +226,14 @@ "playAlertVideos": { "label": "ניגון סרטוני התראות", "desc": "כברירת מחדל, התראות אחרונות בדשבורד שידור חי מופעלות כסרטונים קצרים בלולאה. השבת אפשרות זו כדי להציג רק תמונה סטטית של התראות אחרונות במכשיר/דפדפן זה." + }, + "displayCameraNames": { + "label": "תמיד להציג שם מצלמה", + "desc": "תמיד להציג את שמות המצלמות בצ’יפ בתצוגת הלייב מרובת המצלמות בדשבורד." + }, + "liveFallbackTimeout": { + "label": "זמן המתנה למעבר לנגן חלופי בשידור חי", + "desc": "כאשר זרם השידור חי באיכות גבוהה של מצלמה אינו זמין, המערכת תעבור למצב רוחב־פס נמוך לאחר מספר השניות הזה. ברירת מחדל: 3." } }, "cameraGroupStreaming": { @@ -232,7 +241,7 @@ "title": "הגדרות הזרמת קבוצת מצלמות", "clearAll": "נקה את כל הגדרות השידור" }, - "title": "הגדרות כלליות", + "title": "הגדרות UI", "storedLayouts": { "title": "פריסות תצוגה שמורות", "desc": "ניתן לגרור/לשנות את גודל הפריסה של המצלמות בקבוצת מצלמות. המיקומים נשמרים באחסון המקומי של הדפדפן שלך.", @@ -268,7 +277,9 @@ "notifications": "הגדרת התראות - Frigate", "authentication": "הגדרות אימות - Frigate", "default": "הגדרות - Frigate", - "general": "הגדרות כלליות - Frigate" + "general": "הגדרות ממשק (UI) - Frigate", + "cameraManagement": "ניהול מצלמות - Frigate", + "cameraReview": "הגדרות סקירת מצלמה - Frigate" }, "menu": { "ui": "UI - ממשק משתמש", @@ -279,7 +290,11 @@ "users": "משתמשים", "notifications": "התראות", "frigateplus": "+Frigate", - "enrichments": "תוספות" + "enrichments": "תוספות", + "triggers": "הפעלות", + "cameraManagement": "ניהול", + "cameraReview": "סְקִירָה", + "roles": "תפקידים" }, "dialog": { "unsavedChanges": { @@ -421,7 +436,20 @@ "area": "אזור", "tips": "הפעל אפשרות זו כדי לצייר מלבן על תמונת המצלמה כדי להציג את השטח והיחס שלה. ניתן להשתמש בערכים אלה כדי להגדיר פרמטרים של מסנן צורת אובייקט בתצורה שלך." }, - "desc": "תצוגת ניפוי שגיאות מציגה תצוגה בזמן אמת של אובייקטים במעקב והסטטיסטיקות שלהם. רשימת האובייקטים מציגה סיכום בהשהיית זמן של האובייקטים שזוהו." + "desc": "תצוגת ניפוי שגיאות מציגה תצוגה בזמן אמת של אובייקטים במעקב והסטטיסטיקות שלהם. רשימת האובייקטים מציגה סיכום בהשהיית זמן של האובייקטים שזוהו.", + "openCameraWebUI": "פתח את ממשק ה־Web של {{camera}}", + "audio": { + "title": "אודיו", + "noAudioDetections": "אין זיהויי אודיו", + "score": "ציון", + "currentRMS": "RMS נוכחי", + "currentdbFS": "dbFS נוכחי" + }, + "paths": { + "title": "נתיבים", + "desc": "הצג נקודות משמעותיות במסלול התנועה של האובייקט במעקב", + "tips": "

    נתיבים


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

    " + } }, "users": { "title": "משתמשים", @@ -430,7 +458,7 @@ "desc": "נהל את חשבונות המשתמשים של מופע Frigate זה." }, "addUser": "הוספת משתמש", - "updatePassword": "עדכון סיסמה", + "updatePassword": "איפוס סיסמה", "toast": { "success": { "createUser": "המשתמש {{user}} נוצר בהצלחה", @@ -450,7 +478,7 @@ "role": "הרשאות", "noUsers": "לא נמצאו משתמשים.", "changeRole": "שינוי הרשאות משתמש", - "password": "סיסמה", + "password": "איפוס סיסמה", "deleteUser": "מחיקת משתמש", "username": "שם משתמש" }, @@ -476,7 +504,16 @@ "veryStrong": "מאוד חזק" }, "match": "סיסמאות תואמות", - "notMatch": "הסיסמאות אינן תואמות." + "notMatch": "הסיסמאות אינן תואמות.", + "show": "הצג סיסמה", + "hide": "הסתר סיסמה", + "requirements": { + "title": "דרישות סיסמה:", + "length": "לפחות 8 תווים", + "uppercase": "לפחות אות גדולה אחת", + "digit": "לפחות ספרה אחת", + "special": "לפחות תו מיוחד אחד (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "סיסמה חדשה", @@ -486,7 +523,11 @@ } }, "usernameIsRequired": "נדרש שם משתמש", - "passwordIsRequired": "נדרשת סיסמה" + "passwordIsRequired": "נדרשת סיסמה", + "currentPassword": { + "title": "סיסמה נוכחית", + "placeholder": "הזן את הסיסמה הנוכחית שלך" + } }, "createUser": { "title": "יצירת משתמש חדש", @@ -504,7 +545,12 @@ "doNotMatch": "הסיסמאות אינן תואמות", "updatePassword": "עדכון סיסמה עבור {{username}}", "setPassword": "קבע סיסמה", - "desc": "צור סיסמה חזקה כדי לאבטח חשבון זה." + "desc": "צור סיסמה חזקה כדי לאבטח חשבון זה.", + "currentPasswordRequired": "נדרשת הסיסמה הנוכחית", + "incorrectCurrentPassword": "הסיסמה הנוכחית שגויה", + "passwordVerificationFailed": "נכשל באימות הסיסמה", + "multiDeviceWarning": "כל מכשיר אחר שבו אתה מחובר יידרש להתחבר מחדש בתוך {{refresh_time}}.", + "multiDeviceAdmin": "ניתן גם לאלץ את כל המשתמשים להתחבר מחדש באופן מיידי על־ידי החלפת מפתח ה־JWT שלך." }, "changeRole": { "title": "שינוי הרשאות משתמש", @@ -515,7 +561,8 @@ "admin": "מנהל", "adminDesc": "גישה מלאה לכל התכונות.", "viewer": "צופה", - "viewerDesc": "מוגבל לדשבורד שידור חי, סקירה, גילוי וייצוא בלבד." + "viewerDesc": "מוגבל לדשבורד שידור חי, סקירה, גילוי וייצוא בלבד.", + "customDesc": "תפקיד מותאם אישית עם גישה למצלמות מסוימות." } } } @@ -618,5 +665,454 @@ "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 index 4e21f1a0a..fa32918f6 100644 --- a/web/public/locales/he/views/system.json +++ b/web/public/locales/he/views/system.json @@ -7,7 +7,8 @@ "reindexingEmbeddings": "אינדקס מחדש של ההטמעות ({{processed}}% הושלם)", "cameraIsOffline": "{{camera}} לא זמינה", "detectIsSlow": "{{detect}} איטי ({{speed}} אלפיות שנייה)", - "detectIsVerySlow": "{{detect}} איטי מאוד ({{speed}} אלפיות שנייה)" + "detectIsVerySlow": "{{detect}} איטי מאוד ({{speed}} אלפיות שנייה)", + "shmTooLow": "יש להגדיל את הקצאת ‎/dev/shm‏ ({{total}} MB) לפחות ל־{{min}} MB." }, "documentTitle": { "cameras": "מצב מצלמות - Frigate", @@ -52,7 +53,8 @@ "inferenceSpeed": "מהירות זיהוי", "temperature": "טמפרטורת הגלאי", "cpuUsage": "ניצול מעבד על ידי הגלאי", - "memoryUsage": "שימוש בזיכרון על ידי הגלאי" + "memoryUsage": "שימוש בזיכרון על ידי הגלאי", + "cpuUsageInformation": "המעבד המשמש להכנת נתוני קלט ופלט אל/ממודלי זיהוי. ערך זה אינו מודד את השימוש בהסקה, גם אם נעשה שימוש במעבד גרפי או מאיץ." }, "hardwareInfo": { "gpuMemory": "זיכרון GPU", @@ -85,12 +87,24 @@ } }, "npuUsage": "שימוש ב-NPU", - "npuMemory": "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": "ניצול זיכרון של תהליך" + "processMemoryUsage": "ניצול זיכרון של תהליך", + "series": { + "go2rtc": "go2rtc", + "recording": "מקליט", + "review_segment": "קטע סקירה", + "embeddings": "הטמעות", + "audio_detector": "זיהוי שמע" + } } }, "enrichments": { @@ -107,8 +121,18 @@ "plate_recognition_speed": "מהירות זיהוי לוחית", "text_embedding_speed": "מהירות הטמעת טקסט", "yolov9_plate_detection_speed": "מהירות זיהוי לוחיות YOLOv9", - "yolov9_plate_detection": "זיהוי לוחיות 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": { @@ -129,6 +153,10 @@ "title": "הקלטות", "earliestRecording": "ההקלטה המוקדמת ביותר הזמינה:", "tips": "ערך זה מייצג את סך האחסון בו משתמשים ההקלטות במסד הנתונים של Frigate. Frigate אינו עוקב אחר ניצול האחסון עבור כל הקבצים בדיסק שלך." + }, + "shm": { + "title": "הקצאת SHM (זיכרון משותף)", + "warning": "גודל ה־SHM הנוכחי של {{total}}MB קטן מדי. הגדל אותו לפחות ל־{{min_shm}}MB." } }, "cameras": { diff --git a/web/public/locales/hi/audio.json b/web/public/locales/hi/audio.json index afffaf44a..0705110cf 100644 --- a/web/public/locales/hi/audio.json +++ b/web/public/locales/hi/audio.json @@ -139,5 +139,7 @@ "raindrop": "बारिश की बूंद", "rowboat": "चप्पू वाली नाव", "aircraft": "विमान", - "bicycle": "साइकिल" + "bicycle": "साइकिल", + "bellow": "गर्जना करना", + "motorcycle": "मोटरसाइकिल" } diff --git a/web/public/locales/hi/common.json b/web/public/locales/hi/common.json index 392c9a844..d4c433519 100644 --- a/web/public/locales/hi/common.json +++ b/web/public/locales/hi/common.json @@ -2,6 +2,7 @@ "time": { "untilForTime": "{{time}} तक", "untilForRestart": "जब तक फ्रिगेट पुनः रीस्टार्ट नहीं हो जाता।", - "untilRestart": "रीस्टार्ट होने में" + "untilRestart": "रीस्टार्ट होने में", + "ago": "{{timeAgo}} पहले" } } diff --git a/web/public/locales/hi/components/camera.json b/web/public/locales/hi/components/camera.json index 37c5b27ed..74c05d469 100644 --- a/web/public/locales/hi/components/camera.json +++ b/web/public/locales/hi/components/camera.json @@ -2,6 +2,9 @@ "group": { "label": "कैमरा समूह", "add": "कैमरा समूह जोड़ें", - "edit": "कैमरा समूह संपादित करें" + "edit": "कैमरा समूह संपादित करें", + "delete": { + "label": "कैमरा समूह हटाएँ" + } } } diff --git a/web/public/locales/hi/components/dialog.json b/web/public/locales/hi/components/dialog.json index dce6983b5..bcf4cd070 100644 --- a/web/public/locales/hi/components/dialog.json +++ b/web/public/locales/hi/components/dialog.json @@ -3,7 +3,8 @@ "title": "क्या आप निश्चित हैं कि आप फ्रिगेट को रीस्टार्ट करना चाहते हैं?", "button": "रीस्टार्ट", "restarting": { - "title": "फ्रिगेट रीस्टार्ट हो रहा है" + "title": "फ्रिगेट रीस्टार्ट हो रहा है", + "content": "यह पृष्ठ {{countdown}} सेकंड में पुनः लोड होगा।" } } } diff --git a/web/public/locales/hi/components/filter.json b/web/public/locales/hi/components/filter.json index 214179375..a89133b70 100644 --- a/web/public/locales/hi/components/filter.json +++ b/web/public/locales/hi/components/filter.json @@ -3,7 +3,8 @@ "labels": { "label": "लेबल", "all": { - "title": "सभी लेबल" + "title": "सभी लेबल", + "short": "लेबल" } } } diff --git a/web/public/locales/hi/components/player.json b/web/public/locales/hi/components/player.json index 9b4ed4389..e5e63a82d 100644 --- a/web/public/locales/hi/components/player.json +++ b/web/public/locales/hi/components/player.json @@ -1,5 +1,8 @@ { "noRecordingsFoundForThisTime": "इस समय का कोई रिकॉर्डिंग नहीं मिला", "noPreviewFound": "कोई प्रीव्यू नहीं मिला", - "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला" + "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला", + "submitFrigatePlus": { + "title": "इस फ्रेम को फ्रिगेट+ पर सबमिट करें?" + } } diff --git a/web/public/locales/hi/objects.json b/web/public/locales/hi/objects.json index 436a57668..a4e93c3ab 100644 --- a/web/public/locales/hi/objects.json +++ b/web/public/locales/hi/objects.json @@ -13,5 +13,6 @@ "vehicle": "वाहन", "car": "गाड़ी", "person": "व्यक्ति", - "bicycle": "साइकिल" + "bicycle": "साइकिल", + "motorcycle": "मोटरसाइकिल" } diff --git a/web/public/locales/hi/views/classificationModel.json b/web/public/locales/hi/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/hi/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hi/views/events.json b/web/public/locales/hi/views/events.json index b6fba2aa1..ae2091467 100644 --- a/web/public/locales/hi/views/events.json +++ b/web/public/locales/hi/views/events.json @@ -1,4 +1,8 @@ { "alerts": "अलर्टस", - "detections": "खोजें" + "detections": "खोजें", + "motion": { + "label": "गति", + "only": "केवल गति" + } } diff --git a/web/public/locales/hi/views/explore.json b/web/public/locales/hi/views/explore.json index bb214ba12..daafb9c2d 100644 --- a/web/public/locales/hi/views/explore.json +++ b/web/public/locales/hi/views/explore.json @@ -1,4 +1,8 @@ { "documentTitle": "अन्वेषण करें - फ्रिगेट", - "generativeAI": "जनरेटिव ए आई" + "generativeAI": "जनरेटिव ए आई", + "exploreMore": "और अधिक {{label}} वस्तुओं का अन्वेषण करें", + "exploreIsUnavailable": { + "title": "अन्वेषण अनुपलब्ध है" + } } diff --git a/web/public/locales/hi/views/exports.json b/web/public/locales/hi/views/exports.json index 97a0f0e53..b9e86dac1 100644 --- a/web/public/locales/hi/views/exports.json +++ b/web/public/locales/hi/views/exports.json @@ -1,4 +1,6 @@ { "documentTitle": "निर्यात - फ्रिगेट", - "search": "खोजें" + "search": "खोजें", + "noExports": "कोई निर्यात नहीं मिला", + "deleteExport": "निर्यात हटाएँ" } diff --git a/web/public/locales/hi/views/faceLibrary.json b/web/public/locales/hi/views/faceLibrary.json index 5c8de952e..b30528265 100644 --- a/web/public/locales/hi/views/faceLibrary.json +++ b/web/public/locales/hi/views/faceLibrary.json @@ -1,6 +1,7 @@ { "description": { "addFace": "फेस लाइब्रेरी में नया संग्रह जोड़ने की प्रक्रिया को आगे बढ़ाएं।", - "placeholder": "इस संग्रह का नाम बताएं" + "placeholder": "इस संग्रह का नाम बताएं", + "invalidName": "अमान्य नाम. नाम में केवल अक्षर, संख्याएँ, रिक्त स्थान, एपॉस्ट्रॉफ़ी, अंडरस्कोर और हाइफ़न ही शामिल हो सकते हैं।" } } diff --git a/web/public/locales/hi/views/live.json b/web/public/locales/hi/views/live.json index 86d2a9235..9c7edbeab 100644 --- a/web/public/locales/hi/views/live.json +++ b/web/public/locales/hi/views/live.json @@ -1,4 +1,5 @@ { "documentTitle": "लाइव - फ्रिगेट", - "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट" + "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट", + "lowBandwidthMode": "कम बैंडविड्थ मोड" } diff --git a/web/public/locales/hi/views/search.json b/web/public/locales/hi/views/search.json index b38dd11af..2ea0c8cfe 100644 --- a/web/public/locales/hi/views/search.json +++ b/web/public/locales/hi/views/search.json @@ -1,4 +1,5 @@ { "search": "खोजें", - "savedSearches": "सहेजी गई खोजें" + "savedSearches": "सहेजी गई खोजें", + "searchFor": "{{inputValue}} खोजें" } diff --git a/web/public/locales/hi/views/settings.json b/web/public/locales/hi/views/settings.json index 5fe3a3233..d9bf27ffc 100644 --- a/web/public/locales/hi/views/settings.json +++ b/web/public/locales/hi/views/settings.json @@ -1,6 +1,7 @@ { "documentTitle": { "default": "सेटिंग्स - फ्रिगेट", - "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट" + "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट", + "camera": "कैमरा सेटिंग्स - फ्रिगेट" } } diff --git a/web/public/locales/hi/views/system.json b/web/public/locales/hi/views/system.json index b29ff9abb..23bafa3fc 100644 --- a/web/public/locales/hi/views/system.json +++ b/web/public/locales/hi/views/system.json @@ -1,6 +1,7 @@ { "documentTitle": { "cameras": "कैमरा आँकड़े - फ्रिगेट", - "storage": "भंडारण आँकड़े - फ्रिगेट" + "storage": "भंडारण आँकड़े - फ्रिगेट", + "general": "सामान्य आँकड़े - फ्रिगेट" } } diff --git a/web/public/locales/hr/audio.json b/web/public/locales/hr/audio.json new file mode 100644 index 000000000..55d1e5fce --- /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": "ÄŒamac", + "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 WC-a", + "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 000000000..1b080afe1 --- /dev/null +++ b/web/public/locales/hr/common.json @@ -0,0 +1,305 @@ +{ + "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" + }, + "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 000000000..74a83d2a4 --- /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 000000000..c973fa3f2 --- /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 streamanja kamere", + "title": "{{cameraName}} Streaming Postavke", + "desc": "Promijenite opcije streamanja 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": "Prijenos", + "placeholder": "Izaberi prijenos", + "streamMethod": { + "label": "Metoda Prijenosa", + "placeholder": "Odaberi metodu prijenosa", + "method": { + "noStreaming": { + "label": "Nema Prijenosa", + "desc": "Slike s kamere bit će ažurirane samo jednom u minuti, a prijenos uživo neće biti dostupan." + }, + "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 000000000..42030519d --- /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 000000000..deac5b18b --- /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 000000000..f6c0fa5d5 --- /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 000000000..0df384bea --- /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 000000000..67a1d91a0 --- /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": "Stream nije dostupan", + "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/objects.json b/web/public/locales/hr/objects.json new file mode 100644 index 000000000..955ebe0cd --- /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": "ÄŒamac", + "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/classificationModel.json b/web/public/locales/hr/views/classificationModel.json new file mode 100644 index 000000000..a588d5607 --- /dev/null +++ b/web/public/locales/hr/views/classificationModel.json @@ -0,0 +1,72 @@ +{ + "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": "Nijedan", + "scoreInfo": "Rezultat predstavlja prosječnu klasifikacijsku pouzdanost kroz sve detekcije ovog objekta." + }, + "toast": { + "success": { + "deletedImage": "Obrisane slike", + "deletedCategory": "Izbrisana Klasa", + "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}}" + } + }, + "description": { + "invalidName": "Nevaljano ime. Ime može samo uključivati slova, brojeve, razmake, navodnike, podcrte i crtice." + }, + "train": { + "titleShort": "Nedavno" + }, + "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." + }, + "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." + }, + "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." + }, + "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." + } + } +} diff --git a/web/public/locales/hr/views/configEditor.json b/web/public/locales/hr/views/configEditor.json new file mode 100644 index 000000000..1a5f2d23e --- /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 000000000..3bafeee22 --- /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 000000000..83a7412b0 --- /dev/null +++ b/web/public/locales/hr/views/explore.json @@ -0,0 +1,59 @@ +{ + "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 (embeddings) 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." + } + } + }, + "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" + }, + "trackedObjectsCount_one": "{{count}} praćeni objekt ", + "trackedObjectsCount_few": "{{count}} praćena objekta ", + "trackedObjectsCount_other": "{{count}} praćenih objekata " +} diff --git a/web/public/locales/hr/views/exports.json b/web/public/locales/hr/views/exports.json new file mode 100644 index 000000000..0762ed6c7 --- /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 000000000..29883ae67 --- /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 000000000..82c150edb --- /dev/null +++ b/web/public/locales/hr/views/live.json @@ -0,0 +1,101 @@ +{ + "documentTitle": "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" + } +} diff --git a/web/public/locales/hr/views/recording.json b/web/public/locales/hr/views/recording.json new file mode 100644 index 000000000..8470b3e3d --- /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/search.json b/web/public/locales/hr/views/search.json new file mode 100644 index 000000000..984c3f37a --- /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 000000000..df374e02f --- /dev/null +++ b/web/public/locales/hr/views/settings.json @@ -0,0 +1,71 @@ +{ + "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": "Uređ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šavač 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" + }, + "motionMasks": { + "point_one": "{{count}} točka", + "point_few": "{{count}} točke", + "point_other": "{{count}} točaka" + }, + "objectMasks": { + "point_one": "{{count}} točka", + "point_few": "{{count}} točke", + "point_other": "{{count}} točaka" + } + }, + "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." + } + } + }, + "general": { + "title": "Postavke Korisničkog Sučelja", + "liveDashboard": { + "title": "Uživo Nadzorna Ploča" + } + } +} diff --git a/web/public/locales/hr/views/system.json b/web/public/locales/hr/views/system.json new file mode 100644 index 000000000..0b4d07df8 --- /dev/null +++ b/web/public/locales/hr/views/system.json @@ -0,0 +1,64 @@ +{ + "documentTitle": { + "cameras": "Statistika kamera - Frigate", + "general": "Generalne statistike - Frigate", + "logs": { + "go2rtc": "Go2RTC Zapisnici- Frigate", + "nginx": "Nginx Zapisnici - Frigate", + "frigate": "Frigate Zapisnici - Frigate" + }, + "storage": "Statistika pohrane - Frigate", + "enrichments": "Statistika obogaćivanja - 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": "Kopirani zapisnici u međuspremnik", + "error": "Nisam mogao kopirati zapisnike u međuspremnik" + }, + "tips": "Zapisnici se prenose s poslužitelja", + "toast": { + "error": { + "fetchingLogsFailed": "Greška dohvaćanja zapisnika: {{errorMessage}}", + "whileStreamingLogs": "Pogreška tijekom prijenosa zapisnika: {{errorMessage}}" + } + } + }, + "metrics": "Metrike sustava", + "general": { + "title": "Općenito", + "detector": { + "title": "Detektori", + "inferenceSpeed": "Brzina izvođenja detektora", + "temperature": "Temperatura Detektora", + "cpuUsage": "Detektorova iskorištenost CPU-a", + "cpuUsageInformation": "CPU korišten za pripremu ulaznih i izlaznih podataka za modele detekcije. Ova vrijednost ne mjeri korištenje tijekom izvođenja modela, čak ni ako se koristi GPU ili akcelerator.", + "memoryUsage": "Detektorova Iskorištenost Memorije" + }, + "hardwareInfo": { + "title": "Informacije o hardveru", + "gpuUsage": "Iskorištenost GPU-a", + "gpuMemory": "GPU Memorija", + "gpuEncoder": "GPU Enkoder", + "gpuDecoder": "GPU Dekoder", + "gpuInfo": { + "vainfoOutput": { + "title": "Ispis Vainfo", + "returnCode": "Povratni kod: {{code}}", + "processOutput": "Ispis procesa:", + "processError": "Greška procesa:" + } + } + } + } +} diff --git a/web/public/locales/hu/audio.json b/web/public/locales/hu/audio.json index 7d5d49bf9..cc73f3ccc 100644 --- a/web/public/locales/hu/audio.json +++ b/web/public/locales/hu/audio.json @@ -149,7 +149,7 @@ "car": "Autó", "bus": "Busz", "motorcycle": "Motor", - "train": "Vonat", + "train": "Betanít", "bicycle": "Bicikli", "scream": "Sikoly", "throat_clearing": "Torokköszörülés", diff --git a/web/public/locales/hu/common.json b/web/public/locales/hu/common.json index c45157b38..99e0450c2 100644 --- a/web/public/locales/hu/common.json +++ b/web/public/locales/hu/common.json @@ -142,7 +142,15 @@ "ro": "Román", "hu": "Magyar", "fi": "Finn", - "th": "Thai" + "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)" }, "uiPlayground": "UI játszótér", "faceLibrary": "Arc Könyvtár", @@ -168,7 +176,7 @@ }, "role": { "viewer": "Néző", - "title": "Szerep", + "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." }, @@ -213,6 +221,14 @@ "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": { @@ -254,5 +270,9 @@ }, "label": { "back": "Vissza" + }, + "readTheDocumentation": "Olvassa el a dokumentációt", + "information": { + "pixels": "{{area}}px" } } diff --git a/web/public/locales/hu/components/auth.json b/web/public/locales/hu/components/auth.json index 37dc3a2e4..43b8e9e17 100644 --- a/web/public/locales/hu/components/auth.json +++ b/web/public/locales/hu/components/auth.json @@ -10,6 +10,7 @@ "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 index e53fb9c18..c5294818d 100644 --- a/web/public/locales/hu/components/camera.json +++ b/web/public/locales/hu/components/camera.json @@ -1,6 +1,6 @@ { "group": { - "label": "Kamera Csoport", + "label": "Kamera Csoportok", "delete": { "confirm": { "desc": "Biztosan törölni akarja a következő kamera csoportot {{name}}?", @@ -66,7 +66,8 @@ "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": { diff --git a/web/public/locales/hu/components/dialog.json b/web/public/locales/hu/components/dialog.json index bff6ade19..c45eac1fc 100644 --- a/web/public/locales/hu/components/dialog.json +++ b/web/public/locales/hu/components/dialog.json @@ -1,10 +1,10 @@ { "restart": { - "title": "Biztosan újra szeretnéd indítani a Frigate-ot?", + "title": "Biztosan újra szeretnéd indítani a Frigate-et?", "button": "Újraindítás", "restarting": { "title": "A Frigate újraindul", - "content": "Az oldal újrtölt {{countdown}} másodperc múlva.", + "content": "Az oldal újratölt {{countdown}} másodperc múlva.", "button": "Erőltetett újraindítás azonnal" } }, @@ -22,7 +22,7 @@ "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 {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Ez a tárgy egy {{translatedLabel}} ({{untranslatedLabel}})?" } } }, @@ -107,7 +107,15 @@ "button": { "markAsReviewed": "Megjelölés áttekintettként", "deleteNow": "Törlés Most", - "export": "Exportálás" + "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 index f4b9b9f39..8af6361f4 100644 --- a/web/public/locales/hu/components/filter.json +++ b/web/public/locales/hu/components/filter.json @@ -97,7 +97,7 @@ "label": "Keresés Forrás", "options": { "description": "Leírás", - "thumbnailImage": "Bélyegkép" + "thumbnailImage": "Indexkép" }, "desc": "Válassza ki, hogy a követett objektumok bélyegképeiben vagy leírásaiban szeretne keresni." }, @@ -121,6 +121,16 @@ "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…" + "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" } } diff --git a/web/public/locales/hu/objects.json b/web/public/locales/hu/objects.json index 5bac94fc3..4b53d161b 100644 --- a/web/public/locales/hu/objects.json +++ b/web/public/locales/hu/objects.json @@ -5,7 +5,7 @@ "motorcycle": "Motor", "airplane": "Repülőgép", "bus": "Busz", - "train": "Vonat", + "train": "Betanít", "boat": "Hajó", "dog": "Kutya", "cat": "Macska", diff --git a/web/public/locales/hu/views/classificationModel.json b/web/public/locales/hu/views/classificationModel.json new file mode 100644 index 000000000..75ef202c6 --- /dev/null +++ b/web/public/locales/hu/views/classificationModel.json @@ -0,0 +1,47 @@ +{ + "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": "Törölt képek", + "deletedModel_one": "Sikeresen törölt {{count}} modellt", + "deletedModel_other": "", + "categorizedImage": "A kép sikeresen osztályozva", + "deletedCategory": "Osztály törlése" + }, + "error": { + "deleteImageFailed": "Törlés 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" + } +} diff --git a/web/public/locales/hu/views/configEditor.json b/web/public/locales/hu/views/configEditor.json index b921c987d..69fa822e9 100644 --- a/web/public/locales/hu/views/configEditor.json +++ b/web/public/locales/hu/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Hiba a konfiguráció mentésekor" } }, - "confirm": "Kilép mentés nélkül?" + "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 index 586953de1..313d8e553 100644 --- a/web/public/locales/hu/views/events.json +++ b/web/public/locales/hu/views/events.json @@ -34,5 +34,9 @@ "markTheseItemsAsReviewed": "Ezen elemek megjelölése áttekintettként", "markAsReviewed": "Megjelölés Áttekintettként", "selected_one": "{{count}} kiválasztva", - "selected_other": "{{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" } diff --git a/web/public/locales/hu/views/explore.json b/web/public/locales/hu/views/explore.json index 9f5cd4814..cf811cdef 100644 --- a/web/public/locales/hu/views/explore.json +++ b/web/public/locales/hu/views/explore.json @@ -27,6 +27,14 @@ "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" } }, "details": { @@ -65,12 +73,14 @@ "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}}" + "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." + "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": { @@ -97,7 +107,10 @@ }, "findSimilar": "Keress Hasonlót" }, - "expandRegenerationMenu": "Újragenerálási menü kiterjesztése" + "expandRegenerationMenu": "Újragenerálási menü kiterjesztése", + "score": { + "label": "Pontszám" + } }, "searchResult": { "deleteTrackedObject": { @@ -203,5 +216,11 @@ "snapshot": "pillanatfelvétel" }, "trackedObjectDetails": "Követett Tárgy Részletei", - "exploreMore": "Fedezzen fel több {{label}} tárgyat" + "exploreMore": "Fedezzen fel több {{label}} tárgyat", + "aiAnalysis": { + "title": "MI-elemzés" + }, + "concerns": { + "label": "Aggodalmak" + } } diff --git a/web/public/locales/hu/views/exports.json b/web/public/locales/hu/views/exports.json index f54c70923..ab07aba94 100644 --- a/web/public/locales/hu/views/exports.json +++ b/web/public/locales/hu/views/exports.json @@ -13,5 +13,11 @@ "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" } } diff --git a/web/public/locales/hu/views/faceLibrary.json b/web/public/locales/hu/views/faceLibrary.json index 4aaef392d..c7fb67547 100644 --- a/web/public/locales/hu/views/faceLibrary.json +++ b/web/public/locales/hu/views/faceLibrary.json @@ -1,7 +1,7 @@ { "renameFace": { "title": "Arc átnevezése", - "desc": "Adjon meg egy új nevet {{name}}-nak/-nek" + "desc": "Adjon meg egy új nevet neki: {{name}}" }, "details": { "subLabelScore": "Alcimke érték", @@ -42,12 +42,12 @@ "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 Tanítás fület az egyes észlelt személyekhez tartozó képek kiválasztására és betanítására.
  • 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." + "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": "Segédlet új gyűjtemény hozzáadásához az arckép könyvtárban." + "addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével." }, "selectFace": "Arc kiválasztása", "deleteFaceLibrary": { @@ -90,7 +90,7 @@ "nofaces": "Nincs elérhető arc", "documentTitle": "Arc könyvtár - Frigate", "train": { - "title": "Vonat", + "title": "Friss felismerések", "empty": "Nincs friss arcfelismerés", "aria": "Válassza ki a tanítást" }, diff --git a/web/public/locales/hu/views/live.json b/web/public/locales/hu/views/live.json index 73a8f81f9..b7a5ff967 100644 --- a/web/public/locales/hu/views/live.json +++ b/web/public/locales/hu/views/live.json @@ -43,7 +43,15 @@ "label": "Kattinston a képre a PTZ kamera középre igazításához" } }, - "presets": "PTZ kamera előzetes beállításai" + "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", @@ -126,6 +134,9 @@ "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": { @@ -135,7 +146,8 @@ "recording": "Felvétel", "audioDetection": "Hang Észlelés", "snapshots": "Pillanatképek", - "autotracking": "Automatikus követés" + "autotracking": "Automatikus követés", + "transcription": "Hang Feliratozás" }, "history": { "label": "Előzmény felvételek megjelenítése" @@ -154,5 +166,20 @@ "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/search.json b/web/public/locales/hu/views/search.json index 185a060e5..488ad43c3 100644 --- a/web/public/locales/hu/views/search.json +++ b/web/public/locales/hu/views/search.json @@ -26,7 +26,8 @@ "max_speed": "Maximális Sebesség", "recognized_license_plate": "Felismert Rendszám", "has_clip": "Van Klip", - "has_snapshot": "Van pillanatképe" + "has_snapshot": "Van pillanatképe", + "attributes": "Tulajdonságok" }, "searchType": { "description": "Leírás", diff --git a/web/public/locales/hu/views/settings.json b/web/public/locales/hu/views/settings.json index 5fc972304..573342eba 100644 --- a/web/public/locales/hu/views/settings.json +++ b/web/public/locales/hu/views/settings.json @@ -6,11 +6,13 @@ "classification": "Osztályozási beállítások - Frigate", "masksAndZones": "Maszk és zónaszerkesztő - Frigate", "object": "Hibakeresés - Frigate", - "general": "Áltlános Beállítások - 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" + "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" }, "menu": { "ui": "UI", @@ -22,7 +24,11 @@ "users": "Felhasználók", "notifications": "Értesítések", "frigateplus": "Frigate+", - "enrichments": "Gazdagítások" + "enrichments": "Extra funkciók", + "triggers": "Triggerek", + "roles": "Szerepkörök", + "cameraManagement": "Menedzsment", + "cameraReview": "Vizsgálat" }, "dialog": { "unsavedChanges": { @@ -253,7 +259,8 @@ "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." + "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", @@ -316,7 +323,7 @@ "username": "Felhasználói név", "password": "Jelszó", "deleteUser": "Felhasználó törlése", - "actions": "Műveletek", + "actions": "Akciók", "role": "Szerepkör", "changeRole": "felhasználói szerepkör módosítása" }, @@ -559,6 +566,19 @@ "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": { @@ -616,6 +636,218 @@ "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" + "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." + } + }, + "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": "" + }, + "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" + } } } diff --git a/web/public/locales/hu/views/system.json b/web/public/locales/hu/views/system.json index 847ac7c83..fffa798a3 100644 --- a/web/public/locales/hu/views/system.json +++ b/web/public/locales/hu/views/system.json @@ -87,7 +87,8 @@ "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" + "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ó", @@ -147,6 +148,10 @@ "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": { @@ -174,7 +179,8 @@ "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}}%)" + "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/id/audio.json b/web/public/locales/id/audio.json index 0d46db1e1..0f759c193 100644 --- a/web/public/locales/id/audio.json +++ b/web/public/locales/id/audio.json @@ -27,5 +27,63 @@ "bicycle": "Sepeda", "bus": "Bis", "train": "Kereta", - "boat": "Kapal" + "boat": "Kapal", + "sneeze": "Bersin", + "run": "Lari", + "footsteps": "Langkah kaki", + "chewing": "Mengunyah", + "biting": "Menggigit", + "stomach_rumble": "Perut Keroncongan", + "burping": "Sendawa", + "hiccup": "Cegukan", + "fart": "Kentut", + "hands": "Tangan", + "heartbeat": "Detak Jantung", + "applause": "Tepuk Tangan", + "chatter": "Obrolan", + "children_playing": "Anak-Anak Bermain", + "animal": "Binatang", + "pets": "Peliharaan", + "dog": "Anjing", + "bark": "Gonggongan", + "howl": "Melolong", + "cat": "Kucing", + "meow": "Meong", + "livestock": "Hewan Ternak", + "horse": "Kuda", + "cattle": "Sapi", + "pig": "Babi", + "goat": "Kambing", + "sheep": "Domba", + "chicken": "Ayam", + "cluck": "Berkokok", + "cock_a_doodle_doo": "Kukuruyuk", + "turkey": "Kalkun", + "duck": "Bebek", + "quack": "Kwek", + "goose": "Angsa", + "wild_animals": "Hewan Liar", + "bird": "Burung", + "pigeon": "Merpati", + "crow": "Gagak", + "owl": "Burung Hantu", + "flapping_wings": "Kepakan Sayap", + "dogs": "Anjing", + "insect": "Serangga", + "cricket": "Jangkrik", + "mosquito": "Nyamuk", + "fly": "Lalat", + "frog": "Katak", + "snake": "Ular", + "music": "Musik", + "musical_instrument": "Alat Musik", + "guitar": "Gitar", + "electric_guitar": "Gitar Elektrik", + "acoustic_guitar": "Gitar Akustik", + "strum": "Genjreng", + "banjo": "Banjo", + "snoring": "Ngorok", + "cough": "Batuk", + "clapping": "Tepukan", + "camera": "Kamera" } diff --git a/web/public/locales/id/common.json b/web/public/locales/id/common.json index afe3a285c..34ededad1 100644 --- a/web/public/locales/id/common.json +++ b/web/public/locales/id/common.json @@ -9,6 +9,8 @@ "untilForTime": "Hingga {{time}}", "last7": "7 hari terakhir", "last14": "14 hari terakhir", - "last30": "30 hari terakhir" - } + "last30": "30 hari terakhir", + "thisWeek": "Minggu Ini" + }, + "readTheDocumentation": "Baca dokumentasi" } diff --git a/web/public/locales/id/components/auth.json b/web/public/locales/id/components/auth.json index 0bc931d99..742e3111a 100644 --- a/web/public/locales/id/components/auth.json +++ b/web/public/locales/id/components/auth.json @@ -4,12 +4,13 @@ "password": "Kata sandi", "login": "Masuk", "errors": { - "usernameRequired": "Wajib Menggunakan Username", - "passwordRequired": "Wajib memakai Password", + "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 index da128850f..9da7f9f2d 100644 --- a/web/public/locales/id/components/camera.json +++ b/web/public/locales/id/components/camera.json @@ -15,7 +15,8 @@ "placeholder": "Masukkan nama…", "errorMessage": { "mustLeastCharacters": "Nama grup kamera minimal harus 2 karakter.", - "exists": "Nama grup kamera sudah ada." + "exists": "Nama grup kamera sudah ada.", + "nameMustNotPeriod": "Nama grup kamera tidak boleh ada titik." } } } diff --git a/web/public/locales/id/components/dialog.json b/web/public/locales/id/components/dialog.json index 5d5f20fb8..1a2c284fb 100644 --- a/web/public/locales/id/components/dialog.json +++ b/web/public/locales/id/components/dialog.json @@ -17,7 +17,9 @@ "review": { "question": { "label": "Konfirmasi label ini untuk Frigate Plus", - "ask_a": "Apakah objek ini adalah sebuah{{label}}?" + "ask_a": "Apakah objek ini adalah sebuah{{label}}?", + "ask_an": "Apakah objek ini {{label}}?", + "ask_full": "Apakah ini object {{untranslatedLabel}} ({{translatedLabel}})?" } } } diff --git a/web/public/locales/id/components/filter.json b/web/public/locales/id/components/filter.json index 0ea01e61b..070963bad 100644 --- a/web/public/locales/id/components/filter.json +++ b/web/public/locales/id/components/filter.json @@ -15,5 +15,13 @@ "title": "Semua Zona", "short": "Zona" } + }, + "classes": { + "label": "Kelas", + "all": { + "title": "Semua Kelas" + }, + "count_one": "{{count}} Kelas", + "count_other": "{{count}} Kelas" } } diff --git a/web/public/locales/id/components/player.json b/web/public/locales/id/components/player.json index 097e50a68..151fd41ce 100644 --- a/web/public/locales/id/components/player.json +++ b/web/public/locales/id/components/player.json @@ -14,7 +14,8 @@ "cameraDisabled": "Kamera dinonaktifkan", "stats": { "streamType": { - "title": "Tipe stream:" + "title": "Tipe stream:", + "short": "Jenis" } } } diff --git a/web/public/locales/id/objects.json b/web/public/locales/id/objects.json index ce7f18a78..43d98cdde 100644 --- a/web/public/locales/id/objects.json +++ b/web/public/locales/id/objects.json @@ -8,5 +8,14 @@ "train": "Kereta", "boat": "Kapal", "traffic_light": "Lampu Lalu Lintas", - "fire_hydrant": "Hidran Kebakaran" + "fire_hydrant": "Hidran Kebakaran", + "animal": "Binatang", + "dog": "Anjing", + "bark": "Gonggongan", + "cat": "Kucing", + "horse": "Kuda", + "goat": "Kambing", + "sheep": "Domba", + "bird": "Burung", + "street_sign": "Rambu Jalan" } diff --git a/web/public/locales/id/views/classificationModel.json b/web/public/locales/id/views/classificationModel.json new file mode 100644 index 000000000..6ea3a7915 --- /dev/null +++ b/web/public/locales/id/views/classificationModel.json @@ -0,0 +1,16 @@ +{ + "documentTitle": "Klasifikasi Model - Frigate", + "details": { + "scoreInfo": "Skor tersebut mewakili rata-rata kepercayaan klasifikasi di seluruh deteksi objek ini." + }, + "button": { + "deleteClassificationAttempts": "Hapus Gambar Klasifikasi", + "renameCategory": "Ubah Nama Kelas", + "deleteCategory": "Hapus Kelas", + "deleteImages": "Hapus Gambar", + "trainModel": "Latih Model", + "addClassification": "Tambah Klasifikasi", + "deleteModels": "Hapus Model", + "editModel": "Ubah Model" + } +} diff --git a/web/public/locales/id/views/configEditor.json b/web/public/locales/id/views/configEditor.json index 871c35180..a4d7baeaa 100644 --- a/web/public/locales/id/views/configEditor.json +++ b/web/public/locales/id/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Gagal menyimpan konfigurasi" } }, - "confirm": "Keluar tanpa menyimpan?" + "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 index f320bae8f..94ee3d47d 100644 --- a/web/public/locales/id/views/events.json +++ b/web/public/locales/id/views/events.json @@ -12,5 +12,48 @@ "motion": "Data gerakan tidak ditemukan" }, "timeline.aria": "Pilih timeline", - "timeline": "Linimasa" + "timeline": "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": "objek", + "trackedObject_other": "objek-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" + }, + "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}} terpilih", + "selected_other": "{{count}} terpilih", + "camera": "Kamera", + "detected": "terdeteksi", + "suspiciousActivity": "Aktivitas Mencurigakan", + "threateningActivity": "Aktivitas yang Mengancam" } diff --git a/web/public/locales/id/views/explore.json b/web/public/locales/id/views/explore.json index de062e132..979ceaf7b 100644 --- a/web/public/locales/id/views/explore.json +++ b/web/public/locales/id/views/explore.json @@ -9,7 +9,8 @@ "estimatedTime": "Perkiraan waktu tersisa:", "finishingShortly": "Selesai sesaat lagi", "step": { - "thumbnailsEmbedded": "Keluku dilampirkan " + "thumbnailsEmbedded": "Keluku dilampirkan ", + "descriptionsEmbedded": "Deskripsi terlampir: " } } }, diff --git a/web/public/locales/id/views/exports.json b/web/public/locales/id/views/exports.json index ebb88a9f7..043c313de 100644 --- a/web/public/locales/id/views/exports.json +++ b/web/public/locales/id/views/exports.json @@ -1,17 +1,23 @@ { "documentTitle": "Expor - Frigate", "search": "Cari", - "noExports": "Tidak bisa mengekspor", + "noExports": "Ekspor tidak ditemukan", "deleteExport": "Hapus Ekspor", "deleteExport.desc": "Apakah Anda yakin ingin menghapus {{exportName}}?", "editExport": { - "title": "Ganti Nama saat Ekspor", - "desc": "Masukkan nama baru untuk mengekspor.", + "title": "Ganti Nama Ekspor", + "desc": "Masukkan nama baru untuk ekspor ini.", "saveExport": "Simpan Ekspor" }, "toast": { "error": { - "renameExportFailed": "Gagal mengganti nama export: {{errorMessage}}" + "renameExportFailed": "Gagal mengganti nama ekspor: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Bagikan Ekspor", + "downloadVideo": "Unduh Video", + "editName": "Ubah nama", + "deleteExport": "Hapus ekspor" } } diff --git a/web/public/locales/id/views/faceLibrary.json b/web/public/locales/id/views/faceLibrary.json index ff1fd4b61..bde637fa0 100644 --- a/web/public/locales/id/views/faceLibrary.json +++ b/web/public/locales/id/views/faceLibrary.json @@ -1,6 +1,6 @@ { "description": { - "addFace": "Tambah ke koleksi Pustaka Wajah.", + "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." }, @@ -18,13 +18,75 @@ "createFaceLibrary": { "desc": "Buat koleksi baru", "title": "Buat Koleksi", - "nextSteps": "Untuk membangun fondasi yang kuat:
  • Gunakan tab Latih untuk memilih dan melatih gambar untuk setiap orang yang terdeteksi.
  • Fokus pada gambar lurus untuk hasil terbaik; hindari melatih gambar yang menangkap wajah pada sudut tertentu.
  • " + "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" + "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" + }, + "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 index 97a733541..51817a84b 100644 --- a/web/public/locales/id/views/live.json +++ b/web/public/locales/id/views/live.json @@ -14,7 +14,8 @@ "move": { "clickMove": { "label": "Klik kotak ini untuk menengahkan kamera", - "enable": "Aktifkan klik untuk bergerak" + "enable": "Aktifkan klik untuk bergerak", + "disable": "Non-aktifkan klik untuk bergerak" } } } diff --git a/web/public/locales/id/views/search.json b/web/public/locales/id/views/search.json index c4c598990..fbca84c8d 100644 --- a/web/public/locales/id/views/search.json +++ b/web/public/locales/id/views/search.json @@ -9,5 +9,10 @@ "filterInformation": "Saring Informasi", "filterActive": "Filter aktif" }, - "trackedObjectId": "Tracked Object ID" + "trackedObjectId": "Tracked Object ID", + "filter": { + "label": { + "cameras": "Kamera" + } + } } diff --git a/web/public/locales/id/views/settings.json b/web/public/locales/id/views/settings.json index 43c59244e..45e49fd1e 100644 --- a/web/public/locales/id/views/settings.json +++ b/web/public/locales/id/views/settings.json @@ -8,6 +8,41 @@ "motionTuner": "Penyetel Gerakan - Frigate", "general": "Frigate - Pengaturan Umum", "object": "Debug - Frigate", - "enrichments": "Frigate - Pengaturan Pengayaan" + "enrichments": "Frigate - Pengaturan Pengayaan", + "cameraManagement": "Pengaturan Kamera - Frigate", + "cameraReview": "Pengaturan Ulasan Kamera - Frigate", + "frigatePlus": "Pengaturan Frigate+ - Frigate" + }, + "menu": { + "cameraManagement": "Manajemen", + "notifications": "Notifikasi", + "ui": "Antarmuka Pengguna", + "enrichments": "Peningkatan", + "cameraReview": "Ulasan", + "motionTuner": "Pengatur Gerak", + "triggers": "Pemicu", + "users": "Pengguna", + "roles": "Peran", + "frigateplus": "Frigate+" + }, + "dialog": { + "unsavedChanges": { + "title": "Anda memiliki perubahan yang belum disimpan.", + "desc": "Apakah Anda ingin menyimpan perubahan Anda sebelum melanjutkan?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Tidak Ada Kamera" + }, + "general": { + "title": "Pengaturan Antarmuka Pengguna", + "liveDashboard": { + "title": "Dashboard Langsung", + "automaticLiveView": { + "label": "Tampilan Langsung Otomatis", + "desc": "Secara otomatis beralih ke tampilan langsung kamera saat aktivitas terdeteksi. Menonaktifkan opsi ini menyebabkan gambar statis kamera di dasbor langsung hanya diperbarui sekali per menit." + } + } } } diff --git a/web/public/locales/id/views/system.json b/web/public/locales/id/views/system.json index 183e7ca34..9a70ff6ca 100644 --- a/web/public/locales/id/views/system.json +++ b/web/public/locales/id/views/system.json @@ -11,5 +11,10 @@ } }, "title": "Sistem", - "metrics": "Metrik sistem" + "metrics": "Metrik sistem", + "logs": { + "download": { + "label": "Unduh Log" + } + } } diff --git a/web/public/locales/it/audio.json b/web/public/locales/it/audio.json index eb9b98a5b..17ca12ce1 100644 --- a/web/public/locales/it/audio.json +++ b/web/public/locales/it/audio.json @@ -49,7 +49,7 @@ "cat": "Gatto", "chicken": "Pollo", "acoustic_guitar": "Chitarra acustica", - "speech": "Discorso", + "speech": "Parlato", "babbling": "Balbettio", "motorcycle": "Motociclo", "yell": "Urlo", @@ -399,7 +399,7 @@ "mechanical_fan": "Ventilatore meccanico", "air_conditioning": "Aria condizionata", "cash_register": "Registratore di cassa", - "single-lens_reflex_camera": "Fotocamera reflex a obiettivo singolo", + "single-lens_reflex_camera": "Telecamera reflex a obiettivo singolo", "tools": "Utensili", "jackhammer": "Martello pneumatico", "sawing": "Segare", @@ -425,5 +425,79 @@ "white_noise": "Rumore bianco", "pink_noise": "Rumore rosa", "field_recording": "Registrazione sul campo", - "scream": "Grido" + "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 index 9e0cb2e77..7967311bd 100644 --- a/web/public/locales/it/common.json +++ b/web/public/locales/it/common.json @@ -87,7 +87,10 @@ "formattedTimestampMonthDayYear": { "12hour": "d MMM, yyyy", "24hour": "d MMM, yyyy" - } + }, + "inProgress": "In corso", + "invalidStartTime": "Ora di inizio non valida", + "invalidEndTime": "Ora di fine non valida" }, "button": { "cancel": "Annulla", @@ -124,7 +127,8 @@ "back": "Indietro", "pictureInPicture": "Immagine nell'immagine", "twoWayTalk": "Comunicazione bidirezionale", - "cameraAudio": "Audio della telecamera" + "cameraAudio": "Audio della telecamera", + "continue": "Continua" }, "unit": { "speed": { @@ -134,10 +138,24 @@ "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" + "back": "Vai indietro", + "hide": "Nascondi {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Nessuna", + "all": "Tutte", + "other": "Altro" }, "menu": { "configuration": "Configurazione", @@ -181,7 +199,15 @@ "it": "Italiano (Italiano)", "yue": "粵語 (Cantonese)", "th": "ไทย (Tailandese)", - "ca": "Català (Catalano)" + "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)" }, "darkMode": { "label": "Modalità scura", @@ -231,7 +257,8 @@ "setPassword": "Imposta password" }, "withSystem": "Sistema", - "faceLibrary": "Raccolta volti" + "faceLibrary": "Raccolta volti", + "classification": "Classificazione" }, "pagination": { "next": { @@ -249,7 +276,7 @@ "title": "Ruolo", "admin": "Amministratore", "viewer": "Spettatore", - "desc": "Gli Amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia di Frigate. Gli Spettatori sono limitati alla sola visualizzazione delle telecamere, rivedono gli oggetti e le registrazioni storiche nell'interfaccia utente." + "desc": "Gli amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia utente di Frigate. Gli spettatori 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.", @@ -271,5 +298,18 @@ "title": "Salva" } }, - "selectItem": "Seleziona {{item}}" + "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" + } } diff --git a/web/public/locales/it/components/auth.json b/web/public/locales/it/components/auth.json index bb6e2200d..f74387766 100644 --- a/web/public/locales/it/components/auth.json +++ b/web/public/locales/it/components/auth.json @@ -10,6 +10,7 @@ "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 index 830cfd0e8..a681de1a5 100644 --- a/web/public/locales/it/components/camera.json +++ b/web/public/locales/it/components/camera.json @@ -29,7 +29,7 @@ "label": "Metodo di trasmissione", "method": { "smartStreaming": { - "label": "Trasmissione intelligente (consigliato)", + "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": { @@ -60,7 +60,8 @@ "desc": "Modifica le opzioni di trasmissione dal vivo per la schermata 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.", diff --git a/web/public/locales/it/components/dialog.json b/web/public/locales/it/components/dialog.json index 683d4ce2f..5e88d1f7d 100644 --- a/web/public/locales/it/components/dialog.json +++ b/web/public/locales/it/components/dialog.json @@ -61,12 +61,13 @@ "export": "Esporta", "selectOrExport": "Seleziona o esporta", "toast": { - "success": "Esportazione avviata correttamente. Visualizza il file nella cartella /exports.", + "success": "Esportazione avviata correttamente. Visualizza il file nella pagina delle esportazioni.", "error": { "failed": "Impossibile avviare l'esportazione: {{error}}", "endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio", "noVaildTimeSelected": "Nessun intervallo di tempo valido selezionato" - } + }, + "view": "Visualizzazione" }, "fromTimeline": { "saveExport": "Salva esportazione", @@ -110,7 +111,8 @@ "button": { "export": "Esporta", "markAsReviewed": "Segna come visto", - "deleteNow": "Elimina ora" + "deleteNow": "Elimina ora", + "markAsUnreviewed": "Segna come non visto" }, "confirmDelete": { "desc": { @@ -122,5 +124,13 @@ "error": "Impossibile eliminare: {{error}}" } } + }, + "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 index dd6ebc1b7..ac5eaebad 100644 --- a/web/public/locales/it/components/filter.json +++ b/web/public/locales/it/components/filter.json @@ -63,7 +63,7 @@ "label": "Cerca la fonte", "desc": "Scegli se cercare nelle miniature o nelle descrizioni degli oggetti tracciati.", "options": { - "thumbnailImage": "Immagine anteprima", + "thumbnailImage": "Immagine in miniatura", "description": "Descrizione" } } @@ -98,7 +98,9 @@ "loadFailed": "Impossibile caricare le targhe riconosciute.", "loading": "Caricamento targhe riconosciute…", "placeholder": "Digita per cercare le targhe…", - "noLicensePlatesFound": "Nessuna targa trovata." + "noLicensePlatesFound": "Nessuna targa trovata.", + "selectAll": "Seleziona tutto", + "clearAll": "Cancella tutto" }, "timeRange": "Intervallo di tempo", "subLabels": { @@ -122,5 +124,17 @@ }, "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/views/classificationModel.json b/web/public/locales/it/views/classificationModel.json new file mode 100644 index 000000000..a35a39172 --- /dev/null +++ b/web/public/locales/it/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "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": "Classe eliminata", + "deletedImage": "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}}" + }, + "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}}" + } + }, + "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 stati mancanti", + "description": "Per ottenere risultati ottimali, si consiglia di selezionare esempi per tutti gli stati. È possibile continuare senza selezionare tutti gli stati, ma il modello non verrà addestrato finché tutti gli stati non avranno immagini. Dopo aver continuato, utilizza la vista Classificazioni recenti per classificare le immagini per gli stati mancanti, quindi addestra il modello." + } + } + }, + "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" +} diff --git a/web/public/locales/it/views/configEditor.json b/web/public/locales/it/views/configEditor.json index 4ce1a7378..f53aaed58 100644 --- a/web/public/locales/it/views/configEditor.json +++ b/web/public/locales/it/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Errore durante il salvataggio della configurazione" } }, - "confirm": "Vuoi uscire senza salvare?" + "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 index e07c7bc6a..f1a9255f7 100644 --- a/web/public/locales/it/views/events.json +++ b/web/public/locales/it/views/events.json @@ -1,14 +1,18 @@ { "alerts": "Avvisi", - "detections": "Rilevamento", + "detections": "Rilevamenti", "motion": { - "label": "Movimento", - "only": "Solo movimento" + "label": "Movimenti", + "only": "Solo movimenti" }, "empty": { "alert": "Non ci sono avvisi da rivedere", "detection": "Non ci sono rilevamenti da rivedere", - "motion": "Nessun dato di movimento trovato" + "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", @@ -35,5 +39,30 @@ "selected": "{{count}} selezionati", "selected_one": "{{count}} selezionati", "selected_other": "{{count}} selezionati", - "detected": "rilevato" + "detected": "rilevato", + "suspiciousActivity": "Attività sospetta", + "threateningActivity": "Attività minacciosa", + "detail": { + "noDataFound": "Nessun dato dettagliato da rivedere", + "aria": "Attiva/disattiva 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" } diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json index 547c6ad0a..498e09465 100644 --- a/web/public/locales/it/views/explore.json +++ b/web/public/locales/it/views/explore.json @@ -52,12 +52,16 @@ "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." + "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}}" + "updatedLPRFailed": "Impossibile aggiornare la targa: {{errorMessage}}", + "audioTranscription": "Impossibile richiedere la trascrizione audio: {{errorMessage}}", + "updatedAttributesFailed": "Impossibile aggiornare gli attributi: {{errorMessage}}" } } }, @@ -98,6 +102,17 @@ "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" } }, "objectLifecycle": { @@ -153,7 +168,9 @@ "snapshot": "istantanea", "object_lifecycle": "ciclo di vita dell'oggetto", "details": "dettagli", - "video": "video" + "video": "video", + "thumbnail": "miniatura", + "tracking_details": "dettagli di tracciamento" }, "itemMenu": { "downloadSnapshot": { @@ -182,11 +199,33 @@ "submitToPlus": { "label": "Invia a Frigate+", "aria": "Invia a Frigate Plus" + }, + "addTrigger": { + "label": "Aggiungi innesco", + "aria": "Aggiungi un innesco 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" } }, "dialog": { "confirmDelete": { - "desc": "L'eliminazione di questo oggetto tracciato rimuove l'istantanea, eventuali incorporamenti salvati e tutte le voci associate al ciclo di vita dell'oggetto. Il filmato registrato di questo oggetto tracciato nella vista Storico NON verrà eliminato.

    Vuoi davvero procedere?", + "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" } }, @@ -198,12 +237,69 @@ "success": "Oggetto tracciato eliminato correttamente." } }, - "tooltip": "Corrispondenza {{type}} al {{confidence}}%" + "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}}" + "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" + } + }, + "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 index 0c42816ef..186647521 100644 --- a/web/public/locales/it/views/exports.json +++ b/web/public/locales/it/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Condividi esportazione", + "downloadVideo": "Scarica video", + "editName": "Modifica nome", + "deleteExport": "Elimina esportazione" } } diff --git a/web/public/locales/it/views/faceLibrary.json b/web/public/locales/it/views/faceLibrary.json index 54fe6adb0..b40e7fbcf 100644 --- a/web/public/locales/it/views/faceLibrary.json +++ b/web/public/locales/it/views/faceLibrary.json @@ -1,7 +1,7 @@ { "selectItem": "Seleziona {{item}}", "description": { - "addFace": "Procedura per aggiungere una nuova raccolta alla Libreria dei Volti.", + "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." }, @@ -16,9 +16,10 @@ "unknown": "Sconosciuto" }, "train": { - "title": "Addestra", - "aria": "Seleziona addestramento", - "empty": "Non ci sono recenti tentativi di riconoscimento facciale" + "title": "Riconoscimenti recenti", + "aria": "Seleziona i riconoscimenti recenti", + "empty": "Non ci sono recenti tentativi di riconoscimento facciale", + "titleShort": "Recente" }, "button": { "addFace": "Aggiungi volto", @@ -38,7 +39,7 @@ "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.", + "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}}" @@ -55,7 +56,7 @@ }, "imageEntry": { "dropActive": "Rilascia l'immagine qui…", - "dropInstructions": "Trascina e rilascia un'immagine qui oppure fai clic per selezionarla", + "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." @@ -63,7 +64,7 @@ }, "createFaceLibrary": { "title": "Crea raccolta", - "nextSteps": "Per costruire una base solida:
  • Usa la scheda Addestra 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.
  • ", + "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" }, diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json index b8a44ae27..c32113e66 100644 --- a/web/public/locales/it/views/live.json +++ b/web/public/locales/it/views/live.json @@ -12,8 +12,8 @@ }, "manualRecording": { "recordDisabledTips": "Poiché la registrazione è disabilitata o limitata nella configurazione di questa telecamera, verrà salvata solo un'istantanea.", - "title": "Registrazione su richiesta", - "tips": "Avvia un evento manuale in base alle impostazioni di conservazione della registrazione di questa telecamera.", + "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." @@ -37,7 +37,8 @@ "cameraEnabled": "Telecamera abilitata", "objectDetection": "Rilevamento di oggetti", "recording": "Registrazione", - "audioDetection": "Rilevamento audio" + "audioDetection": "Rilevamento audio", + "transcription": "Trascrizione audio" }, "history": { "label": "Mostra filmati storici" @@ -82,7 +83,15 @@ "label": "Fai clic nella cornice per centrare la telecamera PTZ" } }, - "presets": "Preimpostazioni della 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", @@ -138,6 +147,9 @@ "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": { @@ -154,5 +166,24 @@ "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." + } + }, + "snapshot": { + "takeSnapshot": "Scarica l'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/search.json b/web/public/locales/it/views/search.json index 873ef007c..97f8000c1 100644 --- a/web/public/locales/it/views/search.json +++ b/web/public/locales/it/views/search.json @@ -25,7 +25,8 @@ "after": "Dopo", "max_speed": "Velocità massima", "recognized_license_plate": "Targa riconosciuta", - "sub_labels": "Sottoetichette" + "sub_labels": "Sottoetichette", + "attributes": "Attributi" }, "tips": { "desc": { diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json index 78b74c3b4..9cdcea5fb 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -7,10 +7,12 @@ "masksAndZones": "Editor di maschere e zone - Frigate", "motionTuner": "Regolatore di movimento - Frigate", "object": "Correzioni - Frigate", - "general": "Impostazioni generali - Frigate", + "general": "Impostazioni interfaccia - Frigate", "frigatePlus": "Impostazioni Frigate+ - Frigate", "notifications": "Impostazioni di notifiche - Frigate", - "enrichments": "Impostazioni di miglioramento - Frigate" + "enrichments": "Impostazioni di miglioramento - Frigate", + "cameraManagement": "Gestisci telecamere - Frigate", + "cameraReview": "Impostazioni revisione telecamera - Frigate" }, "frigatePlus": { "snapshotConfig": { @@ -18,7 +20,7 @@ "table": { "snapshots": "Istantanee", "camera": "Telecamera", - "cleanCopySnapshots": "clean_copy Istantanee" + "cleanCopySnapshots": "Istantanee clean_copy" }, "desc": "Per inviare a Frigate+ è necessario che nella configurazione siano abilitate sia le istantanee che le istantanee clean_copy.", "documentation": "Leggi la documentazione", @@ -101,7 +103,20 @@ "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": { @@ -128,8 +143,8 @@ "add": "Nuova maschera di movimento", "toast": { "success": { - "title": "{{polygonName}} è stato salvato. Riavvia Frigate per applicare le modifiche.", - "noName": "La maschera di movimento è stata salvata. Riavvia Frigate per applicare le modifiche." + "title": "{{polygonName}} è stato salvato.", + "noName": "La maschera di movimento è stata salvata." } } }, @@ -140,7 +155,8 @@ "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." + "mustNotContainPeriod": "Il nome della zona non deve contenere punti.", + "mustHaveAtLeastOneLetter": "Il nome della zona deve contenere almeno una lettera." } }, "distance": { @@ -223,7 +239,7 @@ "name": { "inputPlaceHolder": "Inserisci un nome…", "title": "Nome", - "tips": "Il nome deve essere composto da almeno 2 caratteri e non deve essere il nome di una telecamera o di un'altra zona." + "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", @@ -245,7 +261,7 @@ }, "allObjects": "Tutti gli oggetti", "toast": { - "success": "La zona ({{zoneName}}) è stata salvata. Riavvia Frigate per applicare le modifiche." + "success": "La zona ({{zoneName}}) è stata salvata." } }, "objectMasks": { @@ -267,8 +283,8 @@ }, "toast": { "success": { - "noName": "La maschera oggetto è stata salvata. Riavvia Frigate per applicare le modifiche.", - "title": "{{polygonName}} è stato salvato. Riavvia Frigate per applicare le modifiche." + "noName": "La maschera oggetto è stata salvata.", + "title": "{{polygonName}} è stato salvato." } }, "label": "Maschere di oggetti", @@ -292,7 +308,7 @@ "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 auto sono considerati Avvisi. Puoi perfezionare la categorizzazione degli elementi di revisione configurando le zone desiderate.", + "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." @@ -305,7 +321,7 @@ "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. Rilevamento, registrazione e correzioni non saranno disponibili.
    Nota: questa operazione non disabilita le ritrasmissioni di go2rtc.", + "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", @@ -314,6 +330,44 @@ "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": { @@ -326,7 +380,11 @@ "debug": "Correzioni", "users": "Utenti", "frigateplus": "Frigate+", - "enrichments": "Miglioramenti" + "enrichments": "Miglioramenti", + "triggers": "Inneschi", + "roles": "Ruoli", + "cameraManagement": "Gestione", + "cameraReview": "Rivedi" }, "users": { "dialog": { @@ -336,7 +394,8 @@ "intro": "Seleziona il ruolo appropriato per questo utente:", "admin": "Amministratore", "adminDesc": "Accesso completo a tutte le funzionalità.", - "viewer": "Spettatore" + "viewer": "Spettatore", + "customDesc": "Ruolo personalizzato con accesso specifico alla telecamera." }, "title": "Cambia ruolo utente", "desc": "Aggiorna i permessi per {{username}}", @@ -368,7 +427,16 @@ "title": "Password", "placeholder": "Inserisci la password", "match": "Le password corrispondono", - "notMatch": "Le password non corrispondono" + "notMatch": "Le password non corrispondono", + "show": "Mostra password", + "hide": "Nascondi password", + "requirements": { + "title": "Requisiti password:", + "length": "Almeno 8 caratteri", + "uppercase": "Almeno una lettera maiuscola", + "digit": "Almeno una cifra", + "special": "Almeno un carattere speciale (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "Nuova password", @@ -378,7 +446,11 @@ } }, "usernameIsRequired": "Il nome utente è obbligatorio", - "passwordIsRequired": "La password è obbligatoria" + "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.", @@ -391,11 +463,16 @@ "setPassword": "Imposta password", "desc": "Crea una password complessa per proteggere questo account.", "cannotBeEmpty": "La password non può essere vuota", - "doNotMatch": "Le password non corrispondono" + "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": "Password", + "password": "Reimposta password", "username": "Nome utente", "actions": "Azioni", "role": "Ruolo", @@ -423,21 +500,29 @@ "desc": "Gestisci gli account utente di questa istanza Frigate." }, "addUser": "Aggiungi utente", - "updatePassword": "Aggiorna password" + "updatePassword": "Reimposta password" }, "general": { "liveDashboard": { "automaticLiveView": { - "desc": "Passa automaticamente alla visualizzazione dal vivodi una telecamera quando viene rilevata attività. Disattivando questa opzione, le immagini statiche della telecamera nella schermata dal vivo verranno aggiornate solo una volta al minuto.", + "desc": "Passa automaticamente alla visualizzazione dal vivo di una telecamera quando viene rilevata attività. Disattivando questa opzione, le immagini statiche della telecamera nella schermata 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 nella schermata dal vivo vengono riprodotti come brevi video in ciclo. Disattiva questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser." }, - "title": "Schermata dal vivo" + "title": "Schermata 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 generali", + "title": "Impostazioni interfaccia", "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.", @@ -676,11 +761,553 @@ "title": "Classificazione degli 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 \"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.", + "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 della targa", "readTheDocumentation": "Leggi la documentazione" }, "unsavedChanges": "Modifiche alle impostazioni di miglioramento non salvate", "restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)" + }, + "triggers": { + "documentTitle": "Inneschi", + "management": { + "title": "Inneschi", + "desc": "Gestisci gli inneschi per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." + }, + "addTrigger": "Aggiungi innesco", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Contenuto", + "threshold": "Soglia", + "actions": "Azioni", + "noTriggers": "Nessun innesco configurato per questa telecamera.", + "edit": "Modifica", + "deleteTrigger": "Elimina innesco", + "lastTriggered": "Ultimo innesco" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "actions": { + "alert": "Contrassegna come avviso", + "notification": "Invia notifica", + "sub_label": "Aggiungi sottoetichetta", + "attribute": "Aggiungi attributo" + }, + "dialog": { + "createTrigger": { + "title": "Crea innesco", + "desc": "Crea un innesco per la telecamera {{camera}}" + }, + "editTrigger": { + "title": "Modifica innesco", + "desc": "Modifica le impostazioni per l'innesco della telecamera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimina innesco", + "desc": "Vuoi davvero eliminare l'innesco {{triggerName}}? Questa azione non può essere annullata." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Assegna un nome a questo innesco", + "error": { + "minLength": "Il campo deve contenere almeno 2 caratteri.", + "invalidCharacters": "Il campo può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", + "alreadyExists": "Per questa telecamera esiste già un innesco con questo nome." + }, + "description": "Inserisci un nome o una descrizione univoca per identificare questo innesco" + }, + "enabled": { + "description": "Abilita o disabilita questo innesco" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleziona il tipo di innesco", + "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 innesco 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 innesco. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'innesco." + }, + "actions": { + "title": "Azioni", + "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli inneschi. Le sottoetichette aggiungono il nome dell'innesco all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.", + "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'innesco {{name}} è stato creato correttamente.", + "updateTrigger": "L'innesco {{name}} è stato aggiornato correttamente.", + "deleteTrigger": "L'innesco {{name}} è stato eliminato correttamente." + }, + "error": { + "createTriggerFailed": "Impossibile creare l'innesco: {{errorMessage}}", + "updateTriggerFailed": "Impossibile aggiornare l'innesco: {{errorMessage}}", + "deleteTriggerFailed": "Impossibile eliminare l'innesco: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "La ricerca semantica è disabilitata", + "desc": "Per utilizzare gli inneschi, è necessario abilitare la ricerca semantica." + }, + "wizard": { + "title": "Crea innesco", + "step1": { + "description": "Configura le impostazioni di base per il tuo innesco." + }, + "step2": { + "description": "Imposta il contenuto che attiverà questa azione." + }, + "step3": { + "description": "Configura la soglia e le azioni per questo innesco." + }, + "steps": { + "nameAndType": "Nome e tipo", + "configureData": "Configurare i dati", + "thresholdAndActions": "Soglia e azioni" + } + } + }, + "roles": { + "management": { + "title": "Gestione del ruolo di spettatore", + "desc": "Gestisci i ruoli di spettatori 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 \"spettatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_many": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_other": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", 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 'spettatore', 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. 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 delle revisioni generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli elementi da rivedere su questa telecamera." + }, + "review": { + "title": "Rivedi", + "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 di oggetti", + "record": "Registrazione", + "audio": "Audio" + }, + "testStream": "Prova di connessione", + "testSuccess": "Prova del flusso riuscita!", + "testFailed": "Prova del flusso fallita", + "testFailedTitle": "Prova fallita", + "connected": "Connesso", + "notConnected": "Non connesso", + "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://\". Per i flussi di telecamere non RTSP è richiesta la configurazione manuale." + }, + "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": "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": "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." + } + } + } + }, + "cameraManagement": { + "title": "Gestisci telecamere", + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "streams": { + "title": "Abilita/Disabilita telecamere", + "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." + }, + "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" + } } } diff --git a/web/public/locales/it/views/system.json b/web/public/locales/it/views/system.json index ee838403c..d5e92543b 100644 --- a/web/public/locales/it/views/system.json +++ b/web/public/locales/it/views/system.json @@ -65,38 +65,61 @@ "gpuUsage": "Utilizzo GPU", "gpuMemory": "Memoria GPU", "npuUsage": "Utilizzo NPU", - "npuMemory": "Memoria 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." + } }, "detector": { "inferenceSpeed": "Velocità inferenza rilevatore", "title": "Rilevatori", "cpuUsage": "Utilizzo CPU rilevatore", "memoryUsage": "Utilizzo memoria rilevatore", - "temperature": "Temperatura del 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" + "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à incorporazione volti", + "face_embedding_speed": "Velocità incorporamento volti", "plate_recognition_speed": "Velocità riconoscimento targhe", - "image_embedding_speed": "Velocità incorporazione immagini", - "text_embedding_speed": "Velocità incorporazione testo", + "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" + "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" + "infPerSecond": "Inferenze al secondo", + "averageInf": "Tempo medio di inferenza" }, "cameras": { "info": { @@ -121,15 +144,15 @@ "framesAndDetections": "Fotogrammi / Rilevamenti", "label": { "camera": "telecamera", - "detect": "rileva", - "skipped": "saltato", + "detect": "rilevamento", + "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}} rileva", + "cameraDetect": "{{camName}} rilevamento", "cameraFramesPerSecond": "{{camName}} fotogrammi al secondo", "cameraDetectionsPerSecond": "{{camName}} rilevamenti al secondo", "cameraSkippedDetectionsPerSecond": "{{camName}} rilevamenti saltati al secondo", @@ -151,7 +174,8 @@ "reindexingEmbeddings": "Reindicizzazione degli incorporamenti (completata al {{processed}}%)", "cameraIsOffline": "{{camera}} è disconnessa", "detectIsSlow": "{{detect}} è lento ({{speed}} ms)", - "detectIsVerySlow": "{{detect}} è molto lento ({{speed}} ms)" + "detectIsVerySlow": "{{detect}} è molto lento ({{speed}} ms)", + "shmTooLow": "L'allocazione /dev/shm ({{total}} MB) dovrebbe essere aumentata almeno a {{min}} MB." }, "title": "Sistema", "metrics": "Metriche di sistema", @@ -174,6 +198,11 @@ "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" } }, "lastRefreshed": "Ultimo aggiornamento: " diff --git a/web/public/locales/ja/audio.json b/web/public/locales/ja/audio.json index 533e387cc..43811ed76 100644 --- a/web/public/locales/ja/audio.json +++ b/web/public/locales/ja/audio.json @@ -1,5 +1,503 @@ { - "speech": "スピーチ", - "car": "自動車", - "bicycle": "自転車" + "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": "EDM", + "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 index 44b1d2b51..7cef62aa5 100644 --- a/web/public/locales/ja/common.json +++ b/web/public/locales/ja/common.json @@ -1,7 +1,290 @@ { "time": { - "untilForRestart": "Frigateが再起動するまで.", + "untilForRestart": "Frigate が再起動するまで。", "untilRestart": "再起動まで", - "untilForTime": "{{time}} まで" + "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": "終了時刻が無効です" + }, + "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": "すべて" + }, + "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": "続行" + }, + "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": "システム設定に従う" + } + }, + "classification": "分類" + }, + "toast": { + "copyUrlToClipboard": "URLをクリップボードにコピーしました。", + "save": { + "title": "保存", + "error": { + "title": "設定変更の保存に失敗しました: {{errorMessage}}", + "noMessage": "設定変更の保存に失敗しました" + } + } + }, + "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 です" } } diff --git a/web/public/locales/ja/components/auth.json b/web/public/locales/ja/components/auth.json index db2e691c0..d767e3282 100644 --- a/web/public/locales/ja/components/auth.json +++ b/web/public/locales/ja/components/auth.json @@ -2,6 +2,15 @@ "form": { "user": "ユーザー名", "password": "パスワード", - "login": "ログイン" + "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 index e2411e813..4491d0a91 100644 --- a/web/public/locales/ja/components/camera.json +++ b/web/public/locales/ja/components/camera.json @@ -2,6 +2,85 @@ "group": { "label": "カメラグループ", "add": "カメラグループを追加", - "edit": "カメラグループを編集" + "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": "領域" } } diff --git a/web/public/locales/ja/components/dialog.json b/web/public/locales/ja/components/dialog.json index 9b0a3b3bc..629474548 100644 --- a/web/public/locales/ja/components/dialog.json +++ b/web/public/locales/ja/components/dialog.json @@ -1,9 +1,121 @@ { "restart": { - "title": "Frigateを再起動しますか?", + "title": "Frigate を再起動してもよろしいですか?", "restarting": { - "title": "Frigateは再起動中です" + "title": "Frigate を再起動中", + "content": "このページは {{countdown}} 秒後に再読み込みされます。", + "button": "今すぐ強制再読み込み" }, "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_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": "書き出しをプレビュー" + } + }, + "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": "未レビューに戻す" + } + }, + "imagePicker": { + "selectImage": "追跡オブジェクトのサムネイルを選択", + "search": { + "placeholder": "ラベルまたはサブラベルで検索…" + }, + "noImages": "このカメラのサムネイルは見つかりません", + "unknownLabel": "保存済みトリガー画像" } } diff --git a/web/public/locales/ja/components/filter.json b/web/public/locales/ja/components/filter.json index 10c2b4912..bbcc3149d 100644 --- a/web/public/locales/ja/components/filter.json +++ b/web/public/locales/ja/components/filter.json @@ -2,8 +2,139 @@ "labels": { "label": "ラベル", "all": { - "title": "すべてのラベル" + "title": "すべてのラベル", + "short": "ラベル" + }, + "count_one": "{{count}} ラベル", + "count_other": "{{count}} ラベル" + }, + "filter": "フィルター", + "classes": { + "label": "クラス", + "all": { + "title": "すべてのクラス" + }, + "count_one": "{{count}} クラス", + "count_other": "{{count}} クラス" + }, + "zones": { + "label": "ゾーン", + "all": { + "title": "すべてのゾーン", + "short": "ゾーン" } }, - "filter": "フィルタ" + "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/input.json b/web/public/locales/ja/components/input.json index fcf6fccab..22725746e 100644 --- a/web/public/locales/ja/components/input.json +++ b/web/public/locales/ja/components/input.json @@ -1,9 +1,9 @@ { "button": { "downloadVideo": { - "label": "ビデオをダウンロード", + "label": "動画をダウンロード", "toast": { - "success": "あなたのレビュー項目ビデオのダウンロードを開始しました." + "success": "レビュー項目の動画のダウンロードを開始しました。" } } } diff --git a/web/public/locales/ja/components/player.json b/web/public/locales/ja/components/player.json index 153b60804..93befd974 100644 --- a/web/public/locales/ja/components/player.json +++ b/web/public/locales/ja/components/player.json @@ -1,5 +1,51 @@ { "noPreviewFound": "プレビューが見つかりません", - "noRecordingsFoundForThisTime": "この時間帯に録画は見つかりませんでした", - "noPreviewFoundFor": "{{cameraName}} のプレビューが見つかりません" + "noRecordingsFoundForThisTime": "この時間の録画は見つかりません", + "noPreviewFoundFor": "{{cameraName}} のプレビューが見つかりません", + "streamOffline": { + "title": "ストリームオフライン", + "desc": "{{cameraName}} の detect ストリームでフレームが受信されていません。エラーログを確認してください" + }, + "submitFrigatePlus": { + "title": "このフレームを Frigate+ に送信しますか?", + "submit": "送信" + }, + "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+ への送信に失敗しました" + } + } } diff --git a/web/public/locales/ja/objects.json b/web/public/locales/ja/objects.json index 0f9ddaa73..c3e41af3f 100644 --- a/web/public/locales/ja/objects.json +++ b/web/public/locales/ja/objects.json @@ -1,5 +1,120 @@ { "bicycle": "自転車", - "car": "自動車", - "person": "人物" + "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" } diff --git a/web/public/locales/ja/views/classificationModel.json b/web/public/locales/ja/views/classificationModel.json new file mode 100644 index 000000000..e16f1fce5 --- /dev/null +++ b/web/public/locales/ja/views/classificationModel.json @@ -0,0 +1,182 @@ +{ + "documentTitle": "分類モデル - Frigate", + "button": { + "deleteImages": "画像を削除", + "deleteClassificationAttempts": "分類画像を削除", + "renameCategory": "クラス名を変更", + "deleteCategory": "クラスを削除", + "trainModel": "モデルを学習", + "addClassification": "分類を追加", + "deleteModels": "モデルを削除", + "editModel": "モデルを編集" + }, + "toast": { + "success": { + "deletedImage": "削除された画像", + "categorizedImage": "画像の分類に成功しました", + "trainedModel": "モデルを正常に学習させました。", + "trainingModel": "モデルのトレーニングを正常に開始しました。", + "deletedCategory": "クラスを削除しました", + "deletedModel_other": "{{count}} 件のモデルを削除しました", + "updatedModel": "モデル設定を更新しました", + "renamedCategory": "クラス名を {{name}} に変更しました" + }, + "error": { + "deleteImageFailed": "削除に失敗しました: {{errorMessage}}", + "deleteCategoryFailed": "クラスの削除に失敗しました: {{errorMessage}}", + "deleteModelFailed": "モデルの削除に失敗しました: {{errorMessage}}", + "categorizeFailed": "画像の分類に失敗しました: {{errorMessage}}", + "trainingFailed": "モデルの学習に失敗しました。Frigate のログを確認してください。", + "trainingFailedToStart": "モデルの学習を開始できませんでした: {{errorMessage}}", + "updateModelFailed": "モデルの更新に失敗しました: {{errorMessage}}", + "renameCategoryFailed": "クラス名の変更に失敗しました: {{errorMessage}}" + } + }, + "train": { + "titleShort": "Classifications,最近の分類結果を選択,,False,train.aria,,", + "title": "最近の分類結果", + "aria": "最近の分類結果を選択" + }, + "wizard": { + "step1": { + "typeObject": "Classification", + "typeState": "Classification", + "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": "最良の結果を得るため、すべての状態の例を選択することを推奨します。すべてを選択しなくても続行できますが、全状態に画像が揃うまでモデルは学習されません。続行後、「最近の分類」から不足分を分類し、学習を行ってください。" + } + } + }, + "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": "状態モデルを作成" + } + } +} diff --git a/web/public/locales/ja/views/configEditor.json b/web/public/locales/ja/views/configEditor.json index b3d523c94..704c83d0a 100644 --- a/web/public/locales/ja/views/configEditor.json +++ b/web/public/locales/ja/views/configEditor.json @@ -1,7 +1,18 @@ { "copyConfig": "設定をコピー", - "configEditor": "Configエディタ", + "configEditor": "設定エディタ", "saveAndRestart": "保存後再起動", "saveOnly": "保存", - "confirm": "保存せずに終了しますか?" + "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 index f8ac7549c..544412974 100644 --- a/web/public/locales/ja/views/events.json +++ b/web/public/locales/ja/views/events.json @@ -1,7 +1,67 @@ { "detections": "検出", "motion": { - "label": "動作" + "label": "モーション", + "only": "モーションのみ" }, - "alerts": "アラート" + "alerts": "アラート", + "empty": { + "detection": "レビューする検出はありません", + "alert": "レビューするアラートはありません", + "motion": "モーションデータは見つかりません", + "recordingsDisabled": { + "title": "録画を有効にする必要があります", + "description": "カメラの録画が有効になっている場合にのみ、そのカメラに対してレビューアイテムを作成できます。" + } + }, + "camera": "カメラ", + "allCameras": "全カメラ", + "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}} 件選択", + "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/ja/views/explore.json b/web/public/locales/ja/views/explore.json index 0dec7d0b9..35265cc50 100644 --- a/web/public/locales/ja/views/explore.json +++ b/web/public/locales/ja/views/explore.json @@ -1,3 +1,299 @@ { - "generativeAI": "生成AI" + "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": "タイトル" + } + }, + "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": "オブジェクトの移動経路を非表示" + } + }, + "dialog": { + "confirmDelete": { + "title": "削除の確認", + "desc": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連する追跡詳細項目が削除されます。履歴ビューの録画映像は削除されません

    続行してもよろしいですか?" + } + }, + "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": "スコア" + } + }, + "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 index aa8eb6703..3e8ce14d4 100644 --- a/web/public/locales/ja/views/exports.json +++ b/web/public/locales/ja/views/exports.json @@ -1,5 +1,23 @@ { - "documentTitle": "エクスポート - Frigate", - "noExports": "エクスポートがありません", - "search": "検索" + "documentTitle": "書き出し - Frigate", + "noExports": "書き出しは見つかりません", + "search": "検索", + "deleteExport": "書き出しを削除", + "deleteExport.desc": "{{exportName}} を削除してもよろしいですか?", + "editExport": { + "title": "書き出し名を変更", + "desc": "この書き出しの新しい名前を入力してください。", + "saveExport": "書き出しを保存" + }, + "toast": { + "error": { + "renameExportFailed": "書き出し名の変更に失敗しました: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "エクスポートを共有", + "downloadVideo": "動画をダウンロード", + "editName": "名前を編集", + "deleteExport": "エクスポートを削除" + } } diff --git a/web/public/locales/ja/views/faceLibrary.json b/web/public/locales/ja/views/faceLibrary.json index 5bb34f410..5b9392caf 100644 --- a/web/public/locales/ja/views/faceLibrary.json +++ b/web/public/locales/ja/views/faceLibrary.json @@ -1,5 +1,96 @@ { "description": { - "placeholder": "このコレクションの名前を入力してください" + "placeholder": "このコレクションの名前を入力", + "addFace": "最初の画像をアップロードして、フェイスライブラリに新しいコレクションを追加してください。", + "invalidName": "無効な名前です。使用できるのは、英数字、空白、アポストロフィ、アンダースコア、ハイフンのみです。" + }, + "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": "Classifications,最近の分類結果を選択,,False,train.aria,," + }, + "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}})に更新しました。" + }, + "error": { + "uploadingImageFailed": "画像のアップロードに失敗しました: {{errorMessage}}", + "addFaceLibraryFailed": "顔名の設定に失敗しました: {{errorMessage}}", + "deleteFaceFailed": "削除に失敗しました: {{errorMessage}}", + "deleteNameFailed": "名前の削除に失敗しました: {{errorMessage}}", + "renameFaceFailed": "顔の名前変更に失敗しました: {{errorMessage}}", + "trainFailed": "学習に失敗しました: {{errorMessage}}", + "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}" + } } } diff --git a/web/public/locales/ja/views/live.json b/web/public/locales/ja/views/live.json index a279c5391..f88901ab8 100644 --- a/web/public/locales/ja/views/live.json +++ b/web/public/locales/ja/views/live.json @@ -1,5 +1,187 @@ { "documentTitle": "ライブ - Frigate", "documentTitle.withCamera": "{{camera}} - ライブ - Frigate", - "lowBandwidthMode": "低帯域幅モード" + "lowBandwidthMode": "低帯域モード", + "twoWayTalk": { + "enable": "双方向通話を有効化", + "disable": "双方向通話を無効化" + }, + "cameraAudio": { + "enable": "カメラ音声を有効化", + "disable": "カメラ音声を無効化" + }, + "ptz": { + "move": { + "clickMove": { + "label": "フレーム内をクリックしてカメラを中央に移動", + "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": "スナップショットを無効化" + }, + "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": "オートトラッキング" + }, + "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": "このグループ内のカメラを表示する権限がありません。" + } + }, + "snapshot": { + "takeSnapshot": "即時スナップショットをダウンロード", + "noVideoSource": "スナップショットに使用できる映像ソースがありません。", + "captureFailed": "スナップショットの取得に失敗しました。", + "downloadStarted": "スナップショットのダウンロードを開始しました。" + } } diff --git a/web/public/locales/ja/views/recording.json b/web/public/locales/ja/views/recording.json index 336551285..7d76d191f 100644 --- a/web/public/locales/ja/views/recording.json +++ b/web/public/locales/ja/views/recording.json @@ -1,5 +1,12 @@ { - "filter": "フィルタ", + "filter": "フィルター", "calendar": "カレンダー", - "export": "エクスポート" + "export": "書き出し", + "filters": "フィルター", + "toast": { + "error": { + "noValidTimeSelected": "適切な時刻の範囲が選択されていません", + "endTimeMustAfterStartTime": "終了時刻は開始時刻より後である必要があります" + } + } } diff --git a/web/public/locales/ja/views/search.json b/web/public/locales/ja/views/search.json index 02f285695..540606c83 100644 --- a/web/public/locales/ja/views/search.json +++ b/web/public/locales/ja/views/search.json @@ -1,11 +1,73 @@ { - "searchFor": "{{inputValue}} を検索", + "searchFor": "「{{inputValue}}」を検索", "button": { "save": "検索を保存", - "delete": "保存した検索を削除", - "filterInformation": "フィルタ情報", - "clear": "検索をクリア" + "delete": "保存済み検索を削除", + "filterInformation": "フィルター情報", + "clear": "検索をクリア", + "filterActive": "フィルターが有効" }, "search": "検索", - "savedSearches": "保存した検索" + "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 index f0993c869..5c36b1919 100644 --- a/web/public/locales/ja/views/settings.json +++ b/web/public/locales/ja/views/settings.json @@ -1,7 +1,1221 @@ { "documentTitle": { "authentication": "認証設定 - Frigate", - "camera": "カメラの設定 - Frigate", - "default": "設定 - 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" + }, + "menu": { + "ui": "UI", + "enrichments": "高度解析", + "cameras": "カメラ設定", + "masksAndZones": "マスク/ゾーン", + "motionTuner": "モーションチューナー", + "triggers": "トリガー", + "debug": "デバッグ", + "users": "ユーザー", + "notifications": "通知", + "frigateplus": "Frigate+", + "cameraManagement": "管理", + "cameraReview": "レビュー", + "roles": "区分" + }, + "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}}({{label}})", + "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": "保存する前に多角形の作図を完了してください。" + } + } + }, + "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}})を保存しました。" + } + }, + "motionMasks": { + "label": "モーションマスク", + "documentTitle": "モーションマスクを編集 - Frigate", + "desc": { + "title": "モーションマスクは、望ましくない種類の動きで検出がトリガーされるのを防ぎます。過度なマスクはオブジェクト追跡を困難にします。", + "documentation": "ドキュメント" + }, + "add": "新しいモーションマスク", + "edit": "モーションマスクを編集", + "context": { + "title": "モーションマスクは、望ましくない動き(例: 木の枝、カメラのタイムスタンプ)で検出がトリガーされるのを防ぐために使用します。ごく控えめに使用してください。過度なマスクはオブジェクト追跡を困難にします。" + }, + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "polygonAreaTooLarge": { + "title": "モーションマスクがカメラフレームの {{polygonArea}}% を覆っています。大きなモーションマスクは推奨されません。", + "tips": "モーションマスクはオブジェクトの検出自体を防ぎません。代わりに必須ゾーンを使用してください。" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。", + "noName": "モーションマスクを保存しました。" + } + } + }, + "objectMasks": { + "label": "オブジェクトマスク", + "documentTitle": "オブジェクトマスクを編集 - Frigate", + "desc": { + "title": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。", + "documentation": "ドキュメント" + }, + "add": "オブジェクトマスクを追加", + "edit": "オブジェクトマスクを編集", + "context": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。", + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "objects": { + "title": "オブジェクト", + "desc": "このオブジェクトマスクに適用するオブジェクトタイプ。", + "allObjectTypes": "すべてのオブジェクトタイプ" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。", + "noName": "オブジェクトマスクを保存しました。" + } + } + } + }, + "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": "8 文字以上", + "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 へアクセスしてください。" + }, + "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+ への送信には、設定でスナップショットと clean_copy スナップショットの両方を有効にする必要があります。", + "cleanCopyWarning": "一部のカメラではスナップショットは有効ですが、クリーンコピーが無効です。これらのカメラから Frigate+ へ画像を送信するには、スナップショット設定で clean_copy を有効にしてください。", + "table": { + "camera": "カメラ", + "snapshots": "スナップショット", + "cleanCopySnapshots": "clean_copy スナップショット" + } + }, + "modelInfo": { + "title": "モデル情報", + "modelType": "モデルタイプ", + "trainDate": "学習日", + "baseModel": "ベースモデル", + "plusModelType": { + "baseModel": "ベースモデル", + "userModel": "ファインチューニング済み" + }, + "supportedDetectors": "対応検出器", + "cameras": "カメラ", + "loading": "モデル情報を読み込み中…", + "error": "モデル情報の読み込みに失敗しました", + "availableModels": "利用可能なモデル", + "loadingAvailableModels": "利用可能なモデルを読み込み中…", + "modelSelect": "ここで Frigate+ 上の利用可能なモデルを選択できます。現在の検出器構成と互換性のあるモデルのみ選択可能です。" + }, + "unsavedChanges": "未保存の Frigate+ 設定の変更", + "restart_required": "再起動が必要です(Frigate+ モデルを変更)", + "toast": { + "success": "Frigate+ 設定を保存しました。変更を適用するには 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 メッセージを送信します。サブラベルは、トリガー名をオブジェクトのラベルに追加します。属性(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://」で始まる必要があります。非 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": "このストリームでは音声が検出されていません。録画には音声が含まれません。" + }, + "ffmpegModuleDescription": "何度か試してもストリームが読み込まれない場合は、このオプションを有効にしてください。有効にすると、Frigate は go2rtc と併用して ffmpeg モジュールを使用します。一部のカメラストリームでは、互換性が向上する場合があります。" + } + }, + "cameraManagement": { + "title": "カメラ管理", + "addCamera": "新しいカメラを追加", + "editCamera": "カメラを編集:", + "selectCamera": "カメラを選択", + "backToSettings": "カメラ設定に戻る", + "streams": { + "title": "カメラの有効化/無効化", + "desc": "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": "少なくとも1つのロールが必要です", + "rolesUnique": "各ロール(audio、detect、record)は1つのストリームにのみ割り当て可能です", + "addInput": "入力ストリームを追加", + "removeInput": "入力ストリームを削除", + "inputsRequired": "少なくとも1つの入力ストリームが必要です" + }, + "go2rtcStreams": "go2rtc ストリーム", + "streamUrls": "ストリーム URL", + "addUrl": "URL を追加", + "addGo2rtcStream": "go2rtc ストリームを追加", + "toast": { + "success": "カメラ {{cameraName}} を保存しました" + } + } + }, + "cameraReview": { + "title": "カメラレビュー設定", + "object_descriptions": { + "title": "生成AIによるオブジェクト説明", + "desc": "このカメラに対する生成AIのオブジェクト説明を一時的に有効/無効にします。無効にすると、このカメラの追跡オブジェクトについてAI生成の説明は要求されません。" + }, + "review_descriptions": { + "title": "生成AIによるレビュー説明", + "desc": "このカメラに対する生成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 を再起動してください。" + } + } } } diff --git a/web/public/locales/ja/views/system.json b/web/public/locales/ja/views/system.json index 2c5d736f2..222b65b3c 100644 --- a/web/public/locales/ja/views/system.json +++ b/web/public/locales/ja/views/system.json @@ -2,6 +2,200 @@ "documentTitle": { "cameras": "カメラ統計 - Frigate", "general": "一般統計 - Frigate", - "storage": "ストレージ統計 - Frigate" + "storage": "ストレージ統計 - 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": "メッセージ" + }, + "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 出力", + "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 が正常に動作していることを確認できます。本問題はパフォーマンスには影響しません。" + } + }, + "otherProcesses": { + "title": "その他のプロセス", + "processCpuUsage": "プロセスの CPU 使用率", + "processMemoryUsage": "プロセスのメモリ使用量" + } + }, + "storage": { + "title": "ストレージ", + "overview": "概要", + "recordings": { + "title": "録画", + "tips": "この値は Frigate のデータベースで録画が使用している総ストレージ量を表します。Frigate はディスク上のすべてのファイルの使用量を追跡しているわけではありません。", + "earliestRecording": "利用可能な最古の録画:" + }, + "shm": { + "title": "SHM(共有メモリ)の割り当て", + "warning": "現在の SHM サイズ {{total}}MB は小さすぎます。少なくとも {{min_shm}}MB に増やしてください。" + }, + "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}} スキップ検出/秒" + }, + "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 ナンバープレート検出", + "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/ko/audio.json b/web/public/locales/ko/audio.json index 0967ef424..d9db04e9f 100644 --- a/web/public/locales/ko/audio.json +++ b/web/public/locales/ko/audio.json @@ -1 +1,72 @@ -{} +{ + "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": "Synthetic Singing", + "rapping": "랩", + "humming": "허밍", + "groan": "신음", + "grunt": "으르렁", + "whistling": "휘파람", + "breathing": "숨쉬는 소리", + "wheeze": "헐떡임", + "gasp": "헐떡임", + "pant": "거친숨", + "snort": "코골이", + "cough": "기침", + "throat_clearing": "목 긁는 소리", + "sneeze": "재채기", + "sniff": "훌쩍", + "run": "달리기", + "shuffle": "Shuffle", + "footsteps": "발소리", + "chewing": "씹는 소리", + "biting": "치는 소리", + "gargling": "가글", + "stomach_rumble": "배 꼬르륵", + "burping": "트림", + "camera": "카메라" +} diff --git a/web/public/locales/ko/common.json b/web/public/locales/ko/common.json index 0967ef424..e5c8ef9a9 100644 --- a/web/public/locales/ko/common.json +++ b/web/public/locales/ko/common.json @@ -1 +1,271 @@ -{} +{ + "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" + } + }, + "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 (English)", + "es": "Español (Spanish)", + "zhCN": "简体中文 (Simplified 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)", + "sr": "Српски (Serbian)", + "sl": "Slovenščina (Slovenian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "시스템 설정 언어 사용" + } + }, + "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": "얼굴 라이브러리" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "피트", + "meters": "미터" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "뒤로" + }, + "button": { + "apply": "적용", + "reset": "리셋", + "done": "완료", + "enabled": "활성화됨", + "enable": "활성화", + "disabled": "비활성화됨", + "disable": "비활성화", + "save": "저장", + "saving": "저장 중…", + "cancel": "취소", + "close": "닫기", + "copy": "복사", + "back": "뒤로", + "history": "히스토리", + "fullscreen": "전체화면", + "exitFullscreen": "전체화면 나가기", + "pictureInPicture": "Picture in Picture", + "twoWayTalk": "양방향 말하기", + "cameraAudio": "카메라 오디오", + "on": "켜기", + "off": "끄기", + "edit": "편집", + "copyCoordinates": "코디네이트 복사", + "delete": "삭제", + "yes": "예", + "no": "아니오", + "download": "다운로드", + "info": "정보", + "suspended": "일시 정지됨", + "unsuspended": "재개", + "play": "재생", + "unselect": "선택 해제", + "export": "내보내기", + "deleteNow": "바로 삭제하기", + "next": "다음" + }, + "toast": { + "copyUrlToClipboard": "클립보드에 URL이 복사되었습니다.", + "save": { + "title": "저장", + "error": { + "title": "설정 저장 실패: {{errorMessage}}", + "noMessage": "설정 저장이 실패했습니다" + } + } + }, + "role": { + "title": "역할", + "admin": "관리자", + "viewer": "감시자", + "desc": "관리자는 Frigate UI에 모든 접근 권한이 있습니다. 감시자는 카메라 감시, 돌아보기, 과거 영상 조회만 가능합니다." + }, + "pagination": { + "label": "나눠보기", + "previous": { + "title": "이전", + "label": "이전 페이지" + }, + "next": { + "title": "다음", + "label": "다음 페이지" + }, + "more": "더 많은 페이지" + }, + "selectItem": "{{item}} 선택", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/web/public/locales/ko/components/auth.json b/web/public/locales/ko/components/auth.json index 0967ef424..65df51e36 100644 --- a/web/public/locales/ko/components/auth.json +++ b/web/public/locales/ko/components/auth.json @@ -1 +1,15 @@ -{} +{ + "form": { + "user": "사용자명", + "password": "비밀번호", + "login": "로그인", + "errors": { + "usernameRequired": "사용자명은 필수입니다", + "passwordRequired": "비밀번호는 필수입니다", + "rateLimit": "너무 많이 시도했습니다. 다음에 다시 시도하세요.", + "loginFailed": "로그인 실패", + "unknownError": "알려지지 않은 에러. 로그를 확인하세요.", + "webUnknownError": "알려지지 않은 에러. 콘솔 로그를 확인하세요." + } + } +} diff --git a/web/public/locales/ko/components/camera.json b/web/public/locales/ko/components/camera.json index 0967ef424..67b1a2ee6 100644 --- a/web/public/locales/ko/components/camera.json +++ b/web/public/locales/ko/components/camera.json @@ -1 +1,86 @@ -{} +{ + "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)" + } +} diff --git a/web/public/locales/ko/components/dialog.json b/web/public/locales/ko/components/dialog.json index 0967ef424..f701526ef 100644 --- a/web/public/locales/ko/components/dialog.json +++ b/web/public/locales/ko/components/dialog.json @@ -1 +1,92 @@ -{} +{ + "restart": { + "title": "Frigate을 정말로 다시 시작할까요?", + "button": "재시작", + "restarting": { + "title": "Frigate이 재시작 중입니다", + "content": "이 페이지는 {{countdown}} 뒤에 새로 고침 됩니다.", + "button": "강제 재시작" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Frigate+에 등록하기" + }, + "review": { + "question": { + "label": "Frigate +에 이 레이블 등록하기" + } + } + }, + "video": { + "viewInHistory": "히스토리 보기" + } + }, + "export": { + "time": { + "fromTimeline": "타임라인에서 선택하기", + "lastHour_other": "지난 시간", + "custom": "커스텀", + "start": { + "title": "시작 시간", + "label": "시작 시간 선택" + }, + "end": { + "title": "종료 시간", + "label": "종료 시간 선택" + } + }, + "name": { + "placeholder": "내보내기 이름" + }, + "select": "선택", + "export": "내보내기", + "selectOrExport": "선택 또는 내보내기", + "toast": { + "success": "내보내기가 성공적으로 시작되었습니다. /exports 폴더에서 파일을 보실 수 있습니다.", + "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": "삭제 확인" + } + } +} diff --git a/web/public/locales/ko/components/filter.json b/web/public/locales/ko/components/filter.json index 0967ef424..942b97c7d 100644 --- a/web/public/locales/ko/components/filter.json +++ b/web/public/locales/ko/components/filter.json @@ -1 +1,39 @@ -{} +{ + "filter": "필터", + "labels": { + "label": "레이블", + "all": { + "title": "모든 레이블", + "short": "레이블" + } + }, + "zones": { + "label": "구역", + "all": { + "title": "모든 구역", + "short": "구역" + } + }, + "dates": { + "selectPreset": "프리셋 선택", + "all": { + "title": "모든 날짜", + "short": "날짜" + } + }, + "timeRange": "시간 구역", + "subLabels": { + "label": "서브 레이블", + "all": "모든 서브 레이블" + }, + "more": "더 많은 필터", + "classes": { + "label": "분류", + "all": { + "title": "모든 분류" + } + }, + "reset": { + "label": "기본값으로 필터 초기화" + } +} diff --git a/web/public/locales/ko/components/icons.json b/web/public/locales/ko/components/icons.json index 0967ef424..fb1b47c03 100644 --- a/web/public/locales/ko/components/icons.json +++ b/web/public/locales/ko/components/icons.json @@ -1 +1,8 @@ -{} +{ + "iconPicker": { + "selectIcon": "아이콘을 선택해주세요", + "search": { + "placeholder": "아이콘 검색 중…" + } + } +} diff --git a/web/public/locales/ko/components/input.json b/web/public/locales/ko/components/input.json index 0967ef424..00a19b702 100644 --- a/web/public/locales/ko/components/input.json +++ b/web/public/locales/ko/components/input.json @@ -1 +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 index 0967ef424..38ef7daac 100644 --- a/web/public/locales/ko/components/player.json +++ b/web/public/locales/ko/components/player.json @@ -1 +1,51 @@ -{} +{ + "submitFrigatePlus": { + "submit": "제출", + "title": "이 프레임을 Frigate+에 제출하시겠습니까?" + }, + "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/objects.json b/web/public/locales/ko/objects.json index 0967ef424..e3506b15d 100644 --- a/web/public/locales/ko/objects.json +++ b/web/public/locales/ko/objects.json @@ -1 +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/classificationModel.json b/web/public/locales/ko/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ko/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ko/views/configEditor.json b/web/public/locales/ko/views/configEditor.json index 0967ef424..bb8a84c2a 100644 --- a/web/public/locales/ko/views/configEditor.json +++ b/web/public/locales/ko/views/configEditor.json @@ -1 +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 index 0967ef424..971494a81 100644 --- a/web/public/locales/ko/views/events.json +++ b/web/public/locales/ko/views/events.json @@ -1 +1,51 @@ -{} +{ + "alerts": "경보", + "detections": "대상 감지", + "motion": { + "label": "움직임 감지", + "only": "움직임 감지만" + }, + "allCameras": "모든 카메라", + "empty": { + "alert": "다시 볼 '경보' 영상이 없습니다", + "detection": "다시 볼 '대상 감지' 영상이 없습니다", + "motion": "움직임 감지 데이터가 없습니다" + }, + "timeline": "타임라인", + "timeline.aria": "타임라인 선택", + "events": { + "label": "이벤트", + "aria": "이벤트 선택", + "noFoundForTimePeriod": "이 시간대에 이벤트가 없습니다." + }, + "detail": { + "noDataFound": "다시 볼 상세 데이터가 없습니다", + "aria": "상세 보기", + "trackedObject_one": "추적 대상", + "trackedObject_other": "추적 대상", + "noObjectDetailData": "상세 보기 데이터가 없습니다." + }, + "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": "위협적인 행동" +} diff --git a/web/public/locales/ko/views/explore.json b/web/public/locales/ko/views/explore.json index 0967ef424..231eade30 100644 --- a/web/public/locales/ko/views/explore.json +++ b/web/public/locales/ko/views/explore.json @@ -1 +1,31 @@ -{} +{ + "documentTitle": "탐색 - Frigate", + "generativeAI": "생성형 AI", + "exploreMore": "{{label}} 더 많은 감지 대상 탐색하기", + "exploreIsUnavailable": { + "title": "탐색을 사용할 수 없습니다", + "embeddingsReindexing": { + "context": "감지 정보 재처리가 완료되면 탐색할 수 있습니다.", + "startingUp": "시작 중…", + "estimatedTime": "예상 남은시간:", + "finishingShortly": "곧 완료됩니다", + "step": { + "thumbnailsEmbedded": "처리된 썸네일: ", + "descriptionsEmbedded": "처리된 설명: ", + "trackedObjectsProcessed": "처리된 추적 감지: " + } + }, + "downloadingModels": { + "context": "Frigate가 시맨틱 검색 기능을 지원하기 위해 필요한 임베딩 모델을 다운로드하고 있습니다. 네트워크 연결 속도에 따라 몇 분 정도 소요될 수 있습니다.", + "setup": { + "visionModel": "Vision model", + "visionModelFeatureExtractor": "Vision model feature extractor", + "textModel": "Text model", + "textTokenizer": "Text tokenizer" + } + } + }, + "details": { + "timestamp": "시간 기록" + } +} diff --git a/web/public/locales/ko/views/exports.json b/web/public/locales/ko/views/exports.json index 0967ef424..f4c902602 100644 --- a/web/public/locales/ko/views/exports.json +++ b/web/public/locales/ko/views/exports.json @@ -1 +1,17 @@ -{} +{ + "documentTitle": "내보내기 - Frigate", + "search": "검색", + "noExports": "내보내기가 없습니다", + "deleteExport": "내보내기 삭제", + "deleteExport.desc": "{{exportName}}을 지우시겠습니까?", + "editExport": { + "title": "내보내기 이름 변경", + "desc": "이 내보내기의 새 이름을 입력하세요.", + "saveExport": "내보내기 저장" + }, + "toast": { + "error": { + "renameExportFailed": "내보내기 이름 변경에 실패했습니다: {{errorMessage}}" + } + } +} diff --git a/web/public/locales/ko/views/faceLibrary.json b/web/public/locales/ko/views/faceLibrary.json index 0967ef424..e1204d852 100644 --- a/web/public/locales/ko/views/faceLibrary.json +++ b/web/public/locales/ko/views/faceLibrary.json @@ -1 +1,84 @@ -{} +{ + "description": { + "placeholder": "이 모음집의 이름을 입력해주세요", + "addFace": "얼굴 라이브러리에 새 모음집 추가하는 방법을 단계별로 알아보세요.", + "invalidName": "잘못된 이름입니다. 이름은 문자, 숫자, 공백, 따옴표 ('), 밑줄 (_), 그리고 붙임표 (-)만 포함이 가능합니다." + }, + "details": { + "person": "사람", + "subLabelScore": "보조 레이블 신뢰도", + "face": "얼굴 상세정보", + "timestamp": "시간 기록", + "unknown": "알 수 없음" + }, + "selectItem": "{{item}} 선택", + "documentTitle": "얼굴 라이브러리 - Frigate", + "uploadFaceImage": { + "title": "얼굴 사진 올리기" + }, + "collections": "모음집", + "createFaceLibrary": { + "title": "모음집 만들기", + "desc": "새로운 모음집 만들기", + "new": "새 얼굴 만들기" + }, + "steps": { + "faceName": "얼굴 이름 입력", + "uploadFace": "얼굴 사진 올리기", + "nextSteps": "다음 단계" + }, + "train": { + "title": "학습", + "aria": "학습 선택" + }, + "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": "얼굴 신뢰도를 성공적으로 업데이트 했습니다." + }, + "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 index 0967ef424..bfc44d18f 100644 --- a/web/public/locales/ko/views/live.json +++ b/web/public/locales/ko/views/live.json @@ -1 +1,183 @@ -{} +{ + "documentTitle": "실시간 보기 - Frigate", + "documentTitle.withCamera": "{{camera}} - 실시간 보기 - Frigate", + "lowBandwidthMode": "저대역폭 모드", + "twoWayTalk": { + "enable": "양방향 말하기 활성화", + "disable": "양방향 말하기 비활성화" + }, + "cameraAudio": { + "enable": "카메라 오디오 활성화", + "disable": "카메라 오디오 비활성화" + }, + "ptz": { + "move": { + "clickMove": { + "label": "클릭해서 카메라 중앙 배치", + "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": "스냅샷 비활성화" + }, + "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": "카메라 추가" + }, + "snapshot": { + "takeSnapshot": "인스턴트 스냅샷 다운로드", + "noVideoSource": "스냅샷 찍을 비디오 소스가 없습니다.", + "captureFailed": "스냅샷 캡쳐를 하지 못했습니다.", + "downloadStarted": "스냅샷 다운로드가 시작됐습니다." + } +} diff --git a/web/public/locales/ko/views/recording.json b/web/public/locales/ko/views/recording.json index 0967ef424..2aa9934de 100644 --- a/web/public/locales/ko/views/recording.json +++ b/web/public/locales/ko/views/recording.json @@ -1 +1,12 @@ -{} +{ + "filter": "필터", + "export": "내보내기", + "calendar": "날짜", + "filters": "필터", + "toast": { + "error": { + "noValidTimeSelected": "올바른 시간 범위를 선택하세요", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다" + } + } +} diff --git a/web/public/locales/ko/views/search.json b/web/public/locales/ko/views/search.json index 0967ef424..f7a6cfd83 100644 --- a/web/public/locales/ko/views/search.json +++ b/web/public/locales/ko/views/search.json @@ -1 +1,7 @@ -{} +{ + "search": "검색", + "savedSearches": "저장된 검색들", + "button": { + "clear": "검색 초기화" + } +} diff --git a/web/public/locales/ko/views/settings.json b/web/public/locales/ko/views/settings.json index 0967ef424..a5b1d5580 100644 --- a/web/public/locales/ko/views/settings.json +++ b/web/public/locales/ko/views/settings.json @@ -1 +1,122 @@ -{} +{ + "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": "일반 설정 - Frigate", + "frigatePlus": "Frigate+ 설정 - Frigate", + "notifications": "알림 설정 - Frigate", + "cameraManagement": "카메라 관리 - Frigate", + "cameraReview": "카메라 다시보기 설정 - Frigate" + }, + "users": { + "table": { + "actions": "액션" + } + }, + "menu": { + "ui": "UI", + "enrichments": "고급", + "cameras": "카메라 설정", + "masksAndZones": "마스크 / 구역", + "motionTuner": "움직임 감지 조정", + "triggers": "트리거", + "debug": "디버그", + "users": "사용자", + "roles": "역할", + "notifications": "알림", + "frigateplus": "Frigate+", + "cameraManagement": "관리", + "cameraReview": "다시보기" + }, + "dialog": { + "unsavedChanges": { + "title": "저장되지 않은 변경 사항이 있습니다.", + "desc": "계속하기 전에 변경 사항을 저장하시겠습니까?" + } + }, + "cameraSetting": { + "camera": "카메라", + "noCamera": "카메라 없음" + }, + "general": { + "title": "일반 세팅", + "liveDashboard": { + "title": "실시간 보기 대시보드", + "automaticLiveView": { + "label": "자동으로 실시간 보기 전환", + "desc": "활동이 감지되면 자동으로 실시간 보기로 전환합니다. 이 옵션을 끄면 대시보드의 카메라 화면은 1분마다 한 번만 갱신됩니다." + }, + "playAlertVideos": { + "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}}" + } + } + }, + "enrichments": { + "title": "고급 설정", + "unsavedChanges": "변경된 고급 설정을 저장하지 않았습니다" + } +} diff --git a/web/public/locales/ko/views/system.json b/web/public/locales/ko/views/system.json index 0967ef424..4ed89d1ce 100644 --- a/web/public/locales/ko/views/system.json +++ b/web/public/locales/ko/views/system.json @@ -1 +1,186 @@ -{} +{ + "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": "메시지" + }, + "tips": "서버에서 로그 스트리밍 중", + "toast": { + "error": { + "fetchingLogsFailed": "로그 가져오기 오류: {{errorMessage}}", + "whileStreamingLogs": "스크리밍 로그 중 오류: {{errorMessage}}" + } + } + }, + "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": "Reindexing embeddings ({{processed}}% complete)", + "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 index 7f9bbc8a4..2e8d481ce 100644 --- a/web/public/locales/lt/audio.json +++ b/web/public/locales/lt/audio.json @@ -34,5 +34,396 @@ "laughter": "Juokas", "snicker": "Kikenimas", "crying": "Verkimas", - "singing": "Dainavimas" + "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" } diff --git a/web/public/locales/lt/common.json b/web/public/locales/lt/common.json index 0f512e147..712e004cf 100644 --- a/web/public/locales/lt/common.json +++ b/web/public/locales/lt/common.json @@ -47,20 +47,64 @@ "second_few": "{{time}} sekundės", "second_other": "{{time}} sekundžių", "formattedTimestamp": { - "12hour": "" - } + "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" }, "unit": { "speed": { - "kph": "kmh" + "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" + "back": "Eiti atgal", + "hide": "Slėpti {{item}}", + "show": "Rodyti {{item}}" }, "button": { "apply": "Pritaikyti", @@ -82,21 +126,22 @@ "pictureInPicture": "Paveikslėlis Paveiksle", "twoWayTalk": "Dvikryptis Kalbėjimas", "cameraAudio": "Kameros Garsas", - "on": "", + "on": "ON", "edit": "Redaguoti", "copyCoordinates": "Kopijuoti koordinates", "delete": "Ištrinti", "yes": "Taip", "no": "Ne", "download": "Atsisiųsti", - "info": "", + "info": "Info", "suspended": "Pristatbdytas", "unsuspended": "Atnaujinti", "play": "Groti", "unselect": "Atžymėti", "export": "Eksportuoti", "deleteNow": "Trinti Dabar", - "next": "Kitas" + "next": "Kitas", + "off": "OFF" }, "menu": { "system": "Sistema", @@ -131,10 +176,24 @@ "hu": "Vengrų", "fi": "Suomių", "da": "Danų", - "sk": "Slovėnų", + "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": { @@ -182,7 +241,8 @@ "anonymous": "neidentifikuotas", "logout": "atsijungti", "setPassword": "Nustatyti Slaptažodi" - } + }, + "uiPlayground": "UI Playground" }, "toast": { "copyUrlToClipboard": "URL nukopijuotas į atmintį.", @@ -209,6 +269,22 @@ "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" } } diff --git a/web/public/locales/lt/components/auth.json b/web/public/locales/lt/components/auth.json index 7b3737040..3ba7d103b 100644 --- a/web/public/locales/lt/components/auth.json +++ b/web/public/locales/lt/components/auth.json @@ -10,6 +10,7 @@ "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 index 11639ade2..2e3ef8a87 100644 --- a/web/public/locales/lt/components/camera.json +++ b/web/public/locales/lt/components/camera.json @@ -7,7 +7,7 @@ "label": "Ištrinti Kamerų Grupę", "confirm": { "title": "Patvirtinti ištrynimą", - "desc": "Ar tikrai norite ištrinti šią kamerų grupę {{name}}?" + "desc": "Esate įsitikinę, kad norite ištrinti šią kamerų grupę {{name}}?" } }, "name": { @@ -15,8 +15,73 @@ "placeholder": "Įveskite pavadinimą…", "errorMessage": { "mustLeastCharacters": "Kamerų grupės pavadinimas turi būti bent 2 simbolių.", - "exists": "Kamerų grupės pavadinimas jau egzistuoja." + "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 index 4feb8d583..ae5760132 100644 --- a/web/public/locales/lt/components/dialog.json +++ b/web/public/locales/lt/components/dialog.json @@ -1,6 +1,6 @@ { "restart": { - "title": "Ar įsitikinę kad norite perkrauti Frigate?", + "title": "Esate įsitikinę, kad norite perkrauti Frigate?", "button": "Perkrauti", "restarting": { "title": "Frigate Persikrauna", @@ -14,12 +14,110 @@ "question": { "ask_a": "Ar šis objektas yra {{label}}?", "ask_an": "Ar šis objektas yra {{label}}?", - "label": "Patvirtinti šią etiketę į Frigate Plus" + "label": "Patvirtinti šią etiketę į Frigate Plus", + "ask_full": "Ar šis objektas yra {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Pateikta" } }, "submitToPlus": { - "label": "Pateiktį į Frigate+" + "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 index fff4ed16b..0f276efc9 100644 --- a/web/public/locales/lt/components/filter.json +++ b/web/public/locales/lt/components/filter.json @@ -15,5 +15,126 @@ "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/objects.json b/web/public/locales/lt/objects.json index bc7499b61..9aa9b5d70 100644 --- a/web/public/locales/lt/objects.json +++ b/web/public/locales/lt/objects.json @@ -105,14 +105,16 @@ "license_plate": "Registracijos Numeris", "package": "Pakuotė", "bbq_grill": "BBQ kepsninė", - "amazon": "", - "usps": "", - "ups": "", - "fedex": "", - "dhl": "", - "an_post": "", - "purolator": "", - "postnl": "", - "nzpost": "", - "postnord": "" + "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/classificationModel.json b/web/public/locales/lt/views/classificationModel.json new file mode 100644 index 000000000..df38fe0eb --- /dev/null +++ b/web/public/locales/lt/views/classificationModel.json @@ -0,0 +1,125 @@ +{ + "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": "Ištrinta Klasę", + "deletedImage": "Ištrinti Nuotraukas", + "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ų." + }, + "title": "Sukurti Naują Klasifikavimą", + "steps": { + "nameAndDefine": "Pavadinimas ir Apibūdinimas", + "stateArea": "Būsenos Plotas" + } + }, + "menu": { + "objects": "Objektai", + "states": "Būsenos" + } +} diff --git a/web/public/locales/lt/views/configEditor.json b/web/public/locales/lt/views/configEditor.json index 996612c22..e9c67dcff 100644 --- a/web/public/locales/lt/views/configEditor.json +++ b/web/public/locales/lt/views/configEditor.json @@ -12,5 +12,7 @@ "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 index 97ad49255..c3e670a16 100644 --- a/web/public/locales/lt/views/events.json +++ b/web/public/locales/lt/views/events.json @@ -22,7 +22,11 @@ "empty": { "alert": "Nėra pranešimų peržiūrai", "detection": "Nėra aptikimų peržiūrai", - "motion": "Duomenų apie judesius nėra" + "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": { @@ -34,5 +38,30 @@ "label": "Pamatyti naujus peržiūros įrašus", "button": "Nauji Įrašai Peržiūrėjimui" }, - "detected": "aptikta" + "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 index 0681c40c7..b68a3233b 100644 --- a/web/public/locales/lt/views/explore.json +++ b/web/public/locales/lt/views/explore.json @@ -5,10 +5,274 @@ "exploreIsUnavailable": { "embeddingsReindexing": { "startingUp": "Paleidžiama…", - "estimatedTime": "Apytikris likęs laikas:" + "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" + "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 užkelti." + }, + "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}}" + } + } + }, + "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}}" + } + }, + "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ą" + } + }, + "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}}" + } + } + }, + "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 index bc2fb2555..dbb5483b7 100644 --- a/web/public/locales/lt/views/exports.json +++ b/web/public/locales/lt/views/exports.json @@ -3,7 +3,7 @@ "documentTitle": "Eksportuoti - Frigate", "noExports": "Eksportuotų įrašų nerasta", "deleteExport": "Ištrinti Eksportuotą Įrašą", - "deleteExport.desc": "Esate įsitikine, kad norite ištrinti {{exportName}}?", + "deleteExport.desc": "Esate įsitikinę, kad norite ištrinti {{exportName}}?", "editExport": { "title": "Pervadinti Eksportuojamą įrašą", "desc": "Įveskite nauja pavadinimą šiam eksportuojamam įrašui.", @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Nepavyko pervadinti eksportuojamo įrašo: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Pasidalinti įrašu", + "downloadVideo": "Atsisiųsti video", + "editName": "Koreguoti pavadinimą", + "deleteExport": "Ištrinti eksportus" } } diff --git a/web/public/locales/lt/views/faceLibrary.json b/web/public/locales/lt/views/faceLibrary.json index d4dce21f3..cd7307a27 100644 --- a/web/public/locales/lt/views/faceLibrary.json +++ b/web/public/locales/lt/views/faceLibrary.json @@ -1,13 +1,102 @@ { "description": { - "addFace": "Apžiūrėkite naujų kolekcijų pridėjimą prie Veidų Bibliotekos.", + "addFace": "Pridėkite naują kolekciją į Veidų Kolekciją įkeldami savo pirmą nuotrauką.", "placeholder": "Įveskite pavadinimą šiai kolekcijai", - "invalidName": "Netinkamas vardas. Vardai gali turėti tik raides, numerius, tarpus, apostrofus, pabraukimus ir brukšnelius." + "invalidName": "Netinkamas vardas. Vardas gali būti sudarytas tik iš raidžiū, skaičių, tarpų, apostrofų, pabraukimų ar brūkšnelių." }, "details": { "person": "Žmogus", "face": "Veido detelės", "timestamp": "Laiko žyma", - "unknown": "Nežinoma" - } + "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 index 5779ff4c9..06f1577f5 100644 --- a/web/public/locales/lt/views/live.json +++ b/web/public/locales/lt/views/live.json @@ -9,5 +9,175 @@ "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ą" + }, + "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/search.json b/web/public/locales/lt/views/search.json index d970b3d2d..eac3b4f55 100644 --- a/web/public/locales/lt/views/search.json +++ b/web/public/locales/lt/views/search.json @@ -12,7 +12,62 @@ "trackedObjectId": "Sekamo Objekto ID", "filter": { "label": { - "cameras": "Kameros" + "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 index 15a9e53c7..3cad3b210 100644 --- a/web/public/locales/lt/views/settings.json +++ b/web/public/locales/lt/views/settings.json @@ -3,10 +3,883 @@ "default": "Nustatymai - Frigate", "authentication": "Autentifikavimo Nustatymai - Frigate", "camera": "Kameros Nustatymai - Frigate", - "object": "Derinti - Frigate", - "general": "Bendrieji Nustatymai - Frigate", + "object": "Debug - Frigate", + "general": "Vartotojo Sąsajos Nustatymai - Frigate", "frigatePlus": "Frigate+ Nustatymai - Frigate", "notifications": "Pranešimų Nustatymai - Frigate", - "motionTuner": "Judesio Derinimas - 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" + }, + "menu": { + "ui": "UI", + "enrichments": "Patobulinimai", + "cameras": "Kameros Nustatymai", + "masksAndZones": "Maskavimai / Zonos", + "motionTuner": "Judesio Derintojas", + "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}} {{label}}", + "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" + }, + "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!" + } } } diff --git a/web/public/locales/lt/views/system.json b/web/public/locales/lt/views/system.json index fb9784cf7..f390d9671 100644 --- a/web/public/locales/lt/views/system.json +++ b/web/public/locales/lt/views/system.json @@ -7,13 +7,185 @@ "go2rtc": "Go2RTC Žurnalas - Frigate", "nginx": "Nginx Žurnalas - Frigate" }, - "general": "Bendroji Statistika - 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" + } + }, + "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" + }, + "infPerSecond": "Išvadų Per Sekundę" } } diff --git a/web/public/locales/lv/audio.json b/web/public/locales/lv/audio.json new file mode 100644 index 000000000..aab12a1b2 --- /dev/null +++ b/web/public/locales/lv/audio.json @@ -0,0 +1,35 @@ +{ + "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" +} diff --git a/web/public/locales/lv/common.json b/web/public/locales/lv/common.json new file mode 100644 index 000000000..3e8d06126 --- /dev/null +++ b/web/public/locales/lv/common.json @@ -0,0 +1,304 @@ +{ + "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" + }, + "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" + }, + "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" + }, + "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" + } + }, + "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" + } + }, + "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 000000000..b8b99fa54 --- /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 000000000..d061f554d --- /dev/null +++ b/web/public/locales/lv/components/camera.json @@ -0,0 +1,86 @@ +{ + "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." + } + } + } + }, + "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" + } +} diff --git a/web/public/locales/lv/components/dialog.json b/web/public/locales/lv/components/dialog.json new file mode 100644 index 000000000..3f52bc65d --- /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 000000000..292946831 --- /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 000000000..180b0b031 --- /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 000000000..f4af8fe4c --- /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 000000000..43f8359f9 --- /dev/null +++ b/web/public/locales/lv/components/player.json @@ -0,0 +1,51 @@ +{ + "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" + }, + "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+" + } + } +} diff --git a/web/public/locales/lv/objects.json b/web/public/locales/lv/objects.json new file mode 100644 index 000000000..981d5cb44 --- /dev/null +++ b/web/public/locales/lv/objects.json @@ -0,0 +1,19 @@ +{ + "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" +} diff --git a/web/public/locales/lv/views/classificationModel.json b/web/public/locales/lv/views/classificationModel.json new file mode 100644 index 000000000..5d26d5b74 --- /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 000000000..286da8e9a --- /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 000000000..77d4d34e5 --- /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 000000000..63a2d2cbc --- /dev/null +++ b/web/public/locales/lv/views/explore.json @@ -0,0 +1,49 @@ +{ + "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:" + } + }, + "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 000000000..73209ce9e --- /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 000000000..f6e254c22 --- /dev/null +++ b/web/public/locales/lv/views/faceLibrary.json @@ -0,0 +1,93 @@ +{ + "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." + }, + "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 000000000..b0f6887c5 --- /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/recording.json b/web/public/locales/lv/views/recording.json new file mode 100644 index 000000000..f2fb16614 --- /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/search.json b/web/public/locales/lv/views/search.json new file mode 100644 index 000000000..b8bb465a9 --- /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 000000000..57c27b436 --- /dev/null +++ b/web/public/locales/lv/views/settings.json @@ -0,0 +1,28 @@ +{ + "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" + } + } + } +} diff --git a/web/public/locales/lv/views/system.json b/web/public/locales/lv/views/system.json new file mode 100644 index 000000000..76db6be13 --- /dev/null +++ b/web/public/locales/lv/views/system.json @@ -0,0 +1,29 @@ +{ + "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" + } + } +} diff --git a/web/public/locales/ml/audio.json b/web/public/locales/ml/audio.json new file mode 100644 index 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ml/components/player.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 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ml/objects.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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ml/views/live.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 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ml/views/recording.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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 index 289d8273f..cdc92c4dd 100644 --- a/web/public/locales/nb-NO/audio.json +++ b/web/public/locales/nb-NO/audio.json @@ -425,5 +425,79 @@ "pink_noise": "Rosa støy", "television": "Fjernsyn", "radio": "Radio", - "scream": "Skrik" + "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 index df446387f..f58f12ea4 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -81,7 +81,10 @@ "formattedTimestampMonthDayYear": { "24hour": "d. MMM, yyyy", "12hour": "d. MMM, yyyy" - } + }, + "inProgress": "Pågår", + "invalidStartTime": "Ugyldig starttid", + "invalidEndTime": "Ugyldig sluttid" }, "button": { "copy": "Kopier", @@ -118,7 +121,8 @@ "unselect": "Fjern valg", "export": "Eksporter", "deleteNow": "Slett nå", - "next": "Neste" + "next": "Neste", + "continue": "Fortsett" }, "menu": { "help": "Hjelp", @@ -139,7 +143,7 @@ "review": "Inspiser", "explore": "Utforsk", "export": "Eksporter", - "uiPlayground": "UI Sandkasse", + "uiPlayground": "UI-Sandkasse", "faceLibrary": "Ansiktsbibliotek", "user": { "title": "Bruker", @@ -190,7 +194,15 @@ "uk": "Українська (Ukrainsk)", "yue": "粵語 (Kantonesisk)", "th": "ไทย (Thai)", - "ca": "Català (Katalansk)" + "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)" }, "appearance": "Utseende", "darkMode": { @@ -211,7 +223,8 @@ "contrast": "Høy kontrast", "default": "Standard", "highcontrast": "Høy kontrast" - } + }, + "classification": "Klassifisering" }, "pagination": { "next": { @@ -233,10 +246,24 @@ "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" + "back": "Gå tilbake", + "hide": "Skjul {{item}}", + "show": "Vis {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alle", + "other": "Andre" }, "toast": { "copyUrlToClipboard": "Nettadresse kopiert til utklippstavlen.", @@ -264,5 +291,18 @@ "title": "404", "desc": "Siden ble ikke funnet" }, - "selectItem": "Velg {{item}}" + "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": ", " + } } diff --git a/web/public/locales/nb-NO/components/auth.json b/web/public/locales/nb-NO/components/auth.json index caf6a2ca6..c59cd4eb8 100644 --- a/web/public/locales/nb-NO/components/auth.json +++ b/web/public/locales/nb-NO/components/auth.json @@ -10,6 +10,7 @@ "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 index d8735926e..750e09e63 100644 --- a/web/public/locales/nb-NO/components/camera.json +++ b/web/public/locales/nb-NO/components/camera.json @@ -51,7 +51,8 @@ }, "stream": "Strøm", "placeholder": "Velg en strøm" - } + }, + "birdseye": "Fugleperspektiv" }, "add": "Legg til kameragruppe", "edit": "Rediger kameragruppe", @@ -76,7 +77,7 @@ "showOptions": "Vis alternativer", "hideOptions": "Skjul alternativer" }, - "boundingBox": "Omsluttende boks", + "boundingBox": "Avgrensningsboks", "timestamp": "Tidsstempel", "zones": "Soner", "mask": "Maske", diff --git a/web/public/locales/nb-NO/components/dialog.json b/web/public/locales/nb-NO/components/dialog.json index 93a65c99d..fb9bb312d 100644 --- a/web/public/locales/nb-NO/components/dialog.json +++ b/web/public/locales/nb-NO/components/dialog.json @@ -29,7 +29,7 @@ "false_other": "Dette er ikke en {{label}}" }, "question": { - "label": "Bekreft denne merkelappen for Frigate Plus", + "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}})?" @@ -56,12 +56,13 @@ } }, "toast": { - "success": "Eksporten startet. Se filen i /exports-mappen.", + "success": "Eksport startet. Se filen på eksportsiden.", "error": { "failed": "Klarte ikke å starte eksport: {{error}}", "noVaildTimeSelected": "Ingen gyldig tidsperiode valgt", "endTimeMustAfterStartTime": "Sluttid må være etter starttid" - } + }, + "view": "Vis" }, "fromTimeline": { "previewExport": "Forhåndsvis eksport", @@ -117,7 +118,16 @@ "button": { "export": "Eksportér", "markAsReviewed": "Merk som inspisert", - "deleteNow": "Slett nå" + "deleteNow": "Slett nå", + "markAsUnreviewed": "Merk som ikke inspisert" } + }, + "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 index 5bcbf5d08..eb0684619 100644 --- a/web/public/locales/nb-NO/components/filter.json +++ b/web/public/locales/nb-NO/components/filter.json @@ -1,30 +1,30 @@ { "filter": "Filter", "labels": { - "label": "Merkelapper", + "label": "Etiketter", "all": { - "title": "Alle masker / soner", - "short": "Merkelapper" + "title": "Alle etiketter", + "short": "Etiketter" }, "count": "{{count}} merkelapper", - "count_other": "{{count}} Merkelapper", - "count_one": "{{count}} Merkelapp" + "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 øyeblikksbilde.

    Sporede objekter uten et øyeblikksbilde kan ikke sendes 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 øyeblikksbilde" + "hasSnapshot": "Har et stillbilde" }, "sort": { "label": "Sorter", "dateAsc": "Dato (Stigende)", "dateDesc": "Dato (Synkende)", - "scoreAsc": "Objektpoengsum (Stigende)", - "scoreDesc": "Objektpoengsum (Synkende)", + "scoreAsc": "Objektscore (Stigende)", + "scoreDesc": "Objektscore (Synkende)", "speedAsc": "Estimert hastighet (Stigende)", "speedDesc": "Estimert hastighet (Synkende)", "relevance": "Relevans" @@ -39,7 +39,7 @@ "title": "Innstillinger", "defaultView": { "title": "Standard visning", - "desc": "Når ingen filtre er valgt, vis et sammendrag av de nyeste sporede objektene per merkelapp, eller vis et ufiltrert rutenett.", + "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" }, @@ -69,7 +69,7 @@ }, "trackedObjectDelete": { "title": "Bekreft sletting", - "desc": "Sletting av disse {{objectLength}} sporede objektene fjerner øyeblikksbildet, 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.", + "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}}" @@ -84,7 +84,9 @@ "title": "Gjenkjente kjennemerker", "loadFailed": "Kunne ikke laste inn gjenkjente kjennemerker.", "loading": "Laster inn gjenkjente kjennemerker…", - "placeholder": "Skriv for å søke etter kjennemerker…" + "placeholder": "Skriv for å søke etter kjennemerker…", + "selectAll": "Velg alle", + "clearAll": "Fjern alle" }, "dates": { "all": { @@ -99,10 +101,10 @@ }, "timeRange": "Tidsrom", "subLabels": { - "label": "Under-Merkelapper", - "all": "Alle under-Merkelapper" + "label": "Underetiketter", + "all": "Alle underetiketter" }, - "score": "Poengsum", + "score": "Score", "estimatedSpeed": "Estimert hastighet ({{unit}})", "cameras": { "all": { @@ -123,5 +125,17 @@ "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/player.json b/web/public/locales/nb-NO/components/player.json index 5396af367..b08459cfc 100644 --- a/web/public/locales/nb-NO/components/player.json +++ b/web/public/locales/nb-NO/components/player.json @@ -37,7 +37,7 @@ "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" + "desc": "Ingen bilder er mottatt på {{cameraName}} detekt-strømmen, sjekk feilloggene" }, "cameraDisabled": "Kameraet er deaktivert", "toast": { diff --git a/web/public/locales/nb-NO/objects.json b/web/public/locales/nb-NO/objects.json index d292b63b8..5c7c5edd2 100644 --- a/web/public/locales/nb-NO/objects.json +++ b/web/public/locales/nb-NO/objects.json @@ -101,7 +101,7 @@ "raccoon": "Vaskebjørn", "robot_lawnmower": "Robotgressklipper", "waste_bin": "Avfallsbeholder", - "on_demand": "På forespørsel", + "on_demand": "Manuelt opptak", "face": "Ansikt", "license_plate": "Kjennemerke", "package": "Pakke", 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 000000000..e7ee73f08 --- /dev/null +++ b/web/public/locales/nb-NO/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "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": "Klasse slettet", + "deletedImage": "Bilder slettet", + "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}}" + }, + "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}}" + } + }, + "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 tilstandseksempler", + "description": "Det anbefales å velge eksempler for alle tilstander for å oppnå best mulig resultat. Du kan fortsette uten å velge alle tilstander, men modellen vil ikke bli trent før alle tilstander har bilder. Etter at du har gått videre, bruk visningen Nylige klassifiseringer for å klassifisere bilder for de manglende tilstandene, og tren deretter modellen." + } + } + }, + "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" +} diff --git a/web/public/locales/nb-NO/views/configEditor.json b/web/public/locales/nb-NO/views/configEditor.json index 09f0b1c69..c0c9253fa 100644 --- a/web/public/locales/nb-NO/views/configEditor.json +++ b/web/public/locales/nb-NO/views/configEditor.json @@ -12,5 +12,7 @@ "copyConfig": "Kopier konfigurasjonen", "saveAndRestart": "Lagre og omstart", "saveOnly": "Kun lagre", - "confirm": "Avslutt uten å lagre?" + "confirm": "Avslutt uten å lagre?", + "safeConfigEditor": "Konfigurasjonsredigering (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 index 70d24e20e..5e77f38ed 100644 --- a/web/public/locales/nb-NO/views/events.json +++ b/web/public/locales/nb-NO/views/events.json @@ -3,7 +3,11 @@ "empty": { "alert": "Det er ingen varsler å inspisere", "detection": "Det er ingen deteksjoner å inspisere", - "motion": "Ingen bevegelsesdata funnet" + "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": "Tidslinje", "events": { @@ -23,7 +27,7 @@ }, "allCameras": "Alle kameraer", "timeline.aria": "Velg tidslinje", - "documentTitle": "Inspiser - Frigate", + "documentTitle": "Inspeksjon - Frigate", "recordings": { "documentTitle": "Opptak - Frigate" }, @@ -34,5 +38,30 @@ "markTheseItemsAsReviewed": "Merk disse elementene som inspiserte", "selected_one": "{{count}} valgt", "selected_other": "{{count}} valgt", - "detected": "detektert" + "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" } diff --git a/web/public/locales/nb-NO/views/explore.json b/web/public/locales/nb-NO/views/explore.json index e95dbfda2..a9fe5230a 100644 --- a/web/public/locales/nb-NO/views/explore.json +++ b/web/public/locales/nb-NO/views/explore.json @@ -19,7 +19,7 @@ "visionModel": "Visjonsmodell", "visionModelFeatureExtractor": "Funksjonsekstraktor for visjonsmodell", "textModel": "Tekstmodell", - "textTokenizer": "Tekst symbolbygger" + "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": { @@ -65,7 +65,7 @@ "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 å bruke endringene dine." + "success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen. Start Frigate på nytt for å aktivere endringene dine." } } }, @@ -87,26 +87,30 @@ }, "toast": { "success": { - "updatedSublabel": "Under-merkelapp oppdatert med suksess.", + "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." + "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 under-merkelapp: {{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 merkelapper: {{objects}}" + "hasMissingObjects": "Juster konfigurasjonen hvis du vil at Frigate skal lagre sporede objekter for følgende etiketter: {{objects}}" } }, "topScore": { - "info": "Den høyeste poengsummen er den høyeste medianverdi for det sporede objektet, så denne kan avvike fra poengsummen som vises på miniatyrbildet for søkeresultatet.", - "label": "Høyeste poengsum" + "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", @@ -124,10 +128,10 @@ }, "regenerateFromThumbnails": "Regenerer fra miniatyrbilder", "tips": { - "descriptionSaved": "Beskrivelse lagret med suksess", + "descriptionSaved": "Beskrivelsen ble lagret", "saveDescriptionFailed": "Feil ved lagring av beskrivelse: {{errorMessage}}" }, - "label": "Merkelapp", + "label": "Etikett", "editLPR": { "title": "Rediger kjennemerke", "descNoLabel": "Skriv inn et nytt kjennemerke for dette sporede objekt", @@ -138,14 +142,25 @@ "zones": "Soner", "timestamp": "Tidsstempel", "expandRegenerationMenu": "Utvid regenereringsmenyen", - "regenerateFromSnapshot": "Regenerer fra øyeblikksbilde", + "regenerateFromSnapshot": "Regenerer fra stillbilde", "editSubLabel": { - "title": "Rediger under-merkelapp", - "desc": "Angi en ny under-merkelapp for denne {{label}}", - "descNoLabel": "Angi en ny under-merkelapp for dette sporede objektet" + "title": "Rediger underetikett", + "desc": "Angi en ny underetikett for \"{{label}}\"", + "descNoLabel": "Angi en ny underetikett for dette sporede objektet" }, "snapshotScore": { - "label": "Øyeblikksbilde poengsum" + "label": "Stillbilde score" + }, + "score": { + "label": "Score" + }, + "editAttributes": { + "title": "Rediger attributter", + "desc": "Velg klassifiseringsattributter for denne {{label}}" + }, + "attributes": "Klassifiseringsattributter", + "title": { + "label": "Tittel" } }, "itemMenu": { @@ -158,8 +173,8 @@ "label": "Last ned video" }, "downloadSnapshot": { - "label": "Last ned øyeblikksbilde", - "aria": "Last ned øyeblikksbilde" + "label": "Last ned stillbilde", + "aria": "Last ned stillbilde" }, "viewObjectLifecycle": { "label": "Vis objektets livssyklus", @@ -175,33 +190,114 @@ "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" } }, "searchResult": { "deleteTrackedObject": { "toast": { "error": "Feil ved sletting av sporet objekt: {{errorMessage}}", - "success": "Sporet objekt ble slettet med suksess." + "success": "Sporet objekt ble slettet." } }, - "tooltip": "Samsvarer {{type}} til {{confidence}}%" + "tooltip": "Samsvarer {{type}} til {{confidence}}%", + "previousTrackedObject": "Forrige sporede objekt", + "nextTrackedObject": "Neste sporede objekt" }, "trackedObjectDetails": "Detaljer om sporet objekt", "type": { "details": "detaljer", - "snapshot": "øyeblikksbilde", + "snapshot": "stillbilde", "video": "video", - "object_lifecycle": "objektets livssyklus" + "object_lifecycle": "objektets livssyklus", + "thumbnail": "miniatyrbilde", + "tracking_details": "sporingsdetaljer" }, "dialog": { "confirmDelete": { "title": "Bekreft sletting", - "desc": "Sletting av dette sporede objektet fjerner øyeblikksbildet, eventuelle lagrede vektorrepresentasjoner og alle tilknyttede livssykloppføringer for objektet. Opptak av dette sporede objektet i Historikk-visningen vil IKKE bli slettet.

    Er du sikker på at du vil fortsette?" + "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?" } }, "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" + "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 avgrensningsboksene 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" + } + }, + "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 avgrensningsboksen 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": "TIPS: Se for deg et hendelsesklipp med en person som går fra venstre mot høyre. Hvis avgrensningsboksen på tidslinjen for hendelsen konsekvent er til venstre for personen, bør verdien reduseres. På samme måte, hvis en person går fra venstre mot høyre og avgrensningsboksen konsekvent er foran personen, bør verdien økes.", + "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 index 2c1fe59a7..4ced2fcdc 100644 --- a/web/public/locales/nb-NO/views/exports.json +++ b/web/public/locales/nb-NO/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Kunne ikke gi nytt navn til eksport: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Del eksport", + "downloadVideo": "Last ned video", + "editName": "Rediger navn", + "deleteExport": "Slett eksport" } } diff --git a/web/public/locales/nb-NO/views/faceLibrary.json b/web/public/locales/nb-NO/views/faceLibrary.json index 9b5ca0288..d2e0f6bd4 100644 --- a/web/public/locales/nb-NO/views/faceLibrary.json +++ b/web/public/locales/nb-NO/views/faceLibrary.json @@ -1,9 +1,9 @@ { "selectItem": "Velg {{item}}", "description": { - "addFace": "Gå gjennom prosessen med å legge til en ny samling i ansiktsbiblioteket.", + "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, apostrofer, understreker og bindestreker." + "invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek." }, "details": { "person": "Person", @@ -11,7 +11,7 @@ "face": "Ansiktsdetaljer", "faceDesc": "Detaljer for sporet objekt som genererte dette ansiktet", "timestamp": "Tidsstempel", - "scoreInfo": "Under-merkelappens poengsum er basert på en vektet sum ut ifra hvor sikre gjenkjenningene av ansiktene er, så den kan avvike fra poengsummen som vises på øyeblikksbildet.", + "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" }, @@ -20,12 +20,13 @@ "new": "Opprett nytt ansikt", "title": "Opprett samling", "desc": "Opprett en ny samling", - "nextSteps": "For å bygge et sterkt grunnlag:
  • Bruk Tren-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.
  • " + "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 tren", - "title": "Tren", - "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning" + "aria": "Velg nylige gjenkjennelser", + "title": "Nylige gjenkjennelser", + "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning", + "titleShort": "Nylige" }, "selectFace": "Velg ansikt", "deleteFaceLibrary": { @@ -38,7 +39,7 @@ "deleteFaceFailed": "Kunne ikke slette: {{errorMessage}}", "uploadingImageFailed": "Kunne ikke laste opp bilde: {{errorMessage}}", "trainFailed": "Kunne ikke trene: {{errorMessage}}", - "updateFaceScoreFailed": "Kunne ikke oppdatere ansiktsskåring: {{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}}" @@ -49,7 +50,7 @@ "deletedName_one": "{{count}} ansikt ble slettet.", "deletedName_other": "{{count}} ansikter ble slettet.", "trainedFace": "Ansiktet ble trent.", - "updatedFaceScore": "Ansiktsskåring ble oppdatert.", + "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}}" @@ -57,7 +58,7 @@ }, "imageEntry": { "dropActive": "Slipp bildet her…", - "dropInstructions": "Dra og slipp et bilde her, eller klikk for å velge", + "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." diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json index 2183cebb9..e23fa9baa 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -35,6 +35,14 @@ "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": { @@ -42,8 +50,8 @@ "disable": "Deaktiver kamera" }, "snapshots": { - "enable": "Aktiver øyeblikksbilder", - "disable": "Deaktiver øyeblikksbilder" + "enable": "Aktiver stillbilder", + "disable": "Deaktiver stillbilder" }, "audioDetect": { "enable": "Aktiver lydregistrering", @@ -54,7 +62,7 @@ "disable": "Deaktiver automatisk sporing" }, "manualRecording": { - "tips": "Start en manuell hendelse basert på kameraets innstillinger for opptaksbevaring.", + "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." @@ -63,15 +71,15 @@ "label": "Vis statistikk", "desc": "Aktiver dette alternativet for å vise strømmestatistikk som et overlegg på kamerastrømmen." }, - "started": "Startet manuelt opptak på forespørsel.", - "end": "Avslutt opptak på forespørsel", - "title": "Opptak på forespørsel", + "started": "Startet manuelt opptak.", + "end": "Avslutt manuelt opptak", + "title": "Manuelt opptak", "debugView": "Feilsøkingsvisning", - "start": "Start opptak på forespørsel", - "failedToStart": "Kunne ikke starte manuelt opptak på forespørsel.", - "recordDisabledTips": "Siden opptak er deaktivert eller begrenset i konfigurasjonen for dette kameraet, vil kun et øyeblikksbilde bli lagret.", - "ended": "Avsluttet manuelt opptak på forespørsel.", - "failedToEnd": "Kunne ikke avslutte manuelt opptak på forespørsel." + "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": { @@ -100,6 +108,9 @@ "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": { @@ -151,8 +162,28 @@ "cameraEnabled": "Kamera aktivert", "objectDetection": "Objektdeteksjon", "recording": "Opptak", - "snapshots": "Øyeblikksbilder", + "snapshots": "Stillbilder", "audioDetection": "Lydregistrering", - "autotracking": "Automatisk sporing" + "autotracking": "Automatisk sporing", + "transcription": "Lydtranskripsjon" + }, + "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." + } } } diff --git a/web/public/locales/nb-NO/views/search.json b/web/public/locales/nb-NO/views/search.json index baf25a900..f25bf709e 100644 --- a/web/public/locales/nb-NO/views/search.json +++ b/web/public/locales/nb-NO/views/search.json @@ -12,20 +12,21 @@ "filter": { "label": { "cameras": "Kameraer", - "labels": "Merkelapper", + "labels": "Etiketter", "search_type": "Søketype", "after": "Etter", - "min_score": "Min. poengsum", - "max_score": "Maks. poengsum", + "min_score": "Min. score", + "max_score": "Maks. score", "min_speed": "Min. hastighet", "zones": "Soner", - "sub_labels": "Under-merkelapper", + "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 øyeblikksbilde" + "has_snapshot": "Har stillbilde", + "attributes": "Attributter" }, "searchType": { "thumbnail": "Miniatyrbilde", @@ -36,8 +37,8 @@ "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 poengsum 'min_score' må være mindre enn eller lik maksimum poengsum 'max_score'.", - "maxScoreMustBeGreaterOrEqualMinScore": "Maksimum poengsum 'max_score' må være større enn eller lik minimum poengsum 'min_score'.", + "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'." } }, diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json index f98f80b23..0ea43438d 100644 --- a/web/public/locales/nb-NO/views/settings.json +++ b/web/public/locales/nb-NO/views/settings.json @@ -6,11 +6,13 @@ "masksAndZones": "Maske- og soneeditor - Frigate", "motionTuner": "Bevegelsesjustering - Frigate", "object": "Test og feilsøk - Frigate", - "general": "Generelle innstillinger - Frigate", + "general": "Innstillinger for brukergrensesnitt - Frigate", "classification": "Klassifiseringsinnstillinger - Frigate", "frigatePlus": "Frigate+ innstillinger - Frigate", - "notifications": "Meldingsvarsler Innstillinger - Frigate", - "enrichments": "Utvidelser Innstillinger - Frigate" + "notifications": "Innstillinger for meldingsvarsler - Frigate", + "enrichments": "Innstillinger for utvidelser - Frigate", + "cameraManagement": "Administrer kameraer - Frigate", + "cameraReview": "Innstillinger for kamerainspeksjon - Frigate" }, "menu": { "classification": "Klassifisering", @@ -22,7 +24,11 @@ "frigateplus": "Frigate+", "ui": "Brukergrensesnitt", "notifications": "Meldingsvarsler", - "enrichments": "Utvidelser" + "enrichments": "Utvidelser", + "triggers": "Utløsere", + "cameraManagement": "Administrasjon", + "cameraReview": "Inspeksjon", + "roles": "Roller" }, "dialog": { "unsavedChanges": { @@ -44,6 +50,14 @@ "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": { @@ -77,7 +91,7 @@ "clearStreamingSettingsFailed": "Kunne ikke fjerne strømmingsinnstillinger: {{errorMessage}}" } }, - "title": "Generelle innstillinger", + "title": "Innstillinger for brukergrensesnitt", "cameraGroupStreaming": { "title": "Strømmingsinnstillinger for kameragrupper", "desc": "Strømmingsinnstillingene lagres lokalt i nettleseren.", @@ -178,7 +192,45 @@ "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": { @@ -199,7 +251,8 @@ "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." + "hasIllegalCharacter": "Sonenavnet inneholder ugyldige tegn.", + "mustHaveAtLeastOneLetter": "Sonenavnet må inneholde minst én bokstav." } }, "distance": { @@ -261,7 +314,7 @@ "name": { "title": "Navn", "inputPlaceHolder": "Skriv inn et navn…", - "tips": "Navnet må være minst 2 tegn langt og må ikke være det samme som et kamera- eller sone-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", @@ -292,7 +345,7 @@ } }, "toast": { - "success": "Sone ({{zoneName}}) er lagret. Start Frigate på nytt for å bruke endringer." + "success": "Sone ({{zoneName}}) er lagret." } }, "motionMasks": { @@ -318,8 +371,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} er lagret. Start Frigate på nytt for å bruke endringene.", - "noName": "Bevegelsesmasken er lagret. Start Frigate på nytt for å bruke endringene." + "title": "{{polygonName}} er lagret.", + "noName": "Bevegelsesmasken er lagret." } } }, @@ -343,8 +396,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} er lagret. Start Frigate på nytt for å bruke endringene.", - "noName": "Objektmasken er lagret. Start Frigate på nytt for å bruke endringene." + "title": "{{polygonName}} er lagret.", + "noName": "Objektmasken er lagret." } } }, @@ -380,7 +433,7 @@ "objectList": "Objektliste", "noObjects": "Ingen objekter", "boundingBoxes": { - "title": "Omsluttende bokser", + "title": "Avgrensningsbokser", "desc": "Vis omsluttende bokser rundt sporede objekter", "colors": { "label": "Farge på omsluttende bokser for objekt", @@ -407,8 +460,8 @@ }, "objectShapeFilterDrawing": { "document": "Se dokumentasjonen ", - "score": "Poengsum", - "ratio": "Forhold", + "score": "Score", + "ratio": "Sideforhold", "area": "Areal", "title": "Tegning av objektformfilter", "desc": "Tegn et rektangel på bildet for å vise areal- og størrelsesforhold", @@ -420,6 +473,19 @@ "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": { @@ -429,7 +495,7 @@ "desc": "Administrer brukerprofiler for denne Frigate-instansen." }, "addUser": "Legg til bruker", - "updatePassword": "Oppdater passord", + "updatePassword": "Nullstill passord", "toast": { "success": { "deleteUser": "Bruker {{user}} ble slettet", @@ -466,7 +532,16 @@ "strong": "Sterkt" }, "match": "Passordene samsvarer", - "notMatch": "Passordene samsvarer ikke" + "notMatch": "Passordene samsvarer ikke", + "show": "Vis passord", + "hide": "Skjul passord", + "requirements": { + "title": "Passordkrav:", + "length": "Minst 8 tegn", + "uppercase": "Minst en stor bokstav", + "digit": "Minst ett tall", + "special": "Minst ett spesialtegn (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "Nytt passord", @@ -476,7 +551,11 @@ } }, "usernameIsRequired": "Brukernavn er påkrevd", - "passwordIsRequired": "Passord 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}}", @@ -486,7 +565,8 @@ "admin": "Administrator", "adminDesc": "Full tilgang til alle funksjoner.", "viewer": "Visningsbruker", - "viewerDesc": "Begrenset til kun Direkte-dashbord, Inspiser, Utforsk og Eksporter." + "viewerDesc": "Begrenset til kun Direkte-dashbord, Inspiser, Utforsk og Eksporter.", + "customDesc": "Tilpasset rolle med spesifikk kameratilgang." }, "select": "Velg en rolle" }, @@ -506,7 +586,12 @@ "setPassword": "Angi passord", "desc": "Opprett et sterkt passord for å sikre denne kontoen.", "cannotBeEmpty": "Passordet kan ikke være tomt", - "doNotMatch": "Passordene samsvarer ikke" + "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 umiddelbart ved å rotere JWT-hemmeligheten din." } }, "table": { @@ -514,7 +599,7 @@ "actions": "Handlinger", "role": "Rolle", "changeRole": "Endre brukerrolle", - "password": "Passord", + "password": "Nullstill passord", "deleteUser": "Slett bruker", "noUsers": "Ingen brukere funnet." } @@ -602,18 +687,18 @@ }, "title": "Frigate+ Innstillinger", "snapshotConfig": { - "title": "Konfigurasjon av øyeblikksbilde", - "desc": "Innsending til Frigate+ krever at både øyeblikksbilder og clean_copy-øyeblikksbilder er aktivert i konfigurasjonen din.", + "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": "Øyeblikksbilder", - "cleanCopySnapshots": "clean_copy-øyeblikksbilder" + "snapshots": "Stillbilder", + "cleanCopySnapshots": "clean_copy-stillbilder" }, - "cleanCopyWarning": "Noen kameraer har øyeblikksbilder aktivert, men ren kopi er deaktivert. Du må aktivere clean_copy i øyeblikksbilde-konfigurasjonen for å kunne sende bilder fra disse kameraene til Frigate+." + "cleanCopyWarning": "Noen kameraer har stillbilder aktivert, men ren kopi er deaktivert. Du må aktivere clean_copy i stillbilde-konfigurasjonen for å kunne sende bilder fra disse kameraene til Frigate+." }, "toast": { - "success": "Frigate+ innstillingene er lagret. Start Frigate på nytt for å bruke endringene.", + "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)", @@ -622,12 +707,12 @@ "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 under-merkelapp 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.", + "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 under-merkelapp. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "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": { @@ -671,14 +756,555 @@ } }, "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 under-merkelapp. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "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 å bruke endringene.", + "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://\". Manuell konfigurering kreves for kamera­strø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 på nytt", + "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." + } + } + } + }, + "cameraManagement": { + "title": "Administrer kameraer", + "addCamera": "Legg til nytt kamera", + "editCamera": "Rediger kamera:", + "selectCamera": "Velg et kamera", + "backToSettings": "Tilbake til kamerainnstillinger", + "streams": { + "title": "Aktiver / Deaktiver kameraer", + "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." + }, + "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" + } + } + }, + "cameraReview": { + "title": "Innstillinger for kamerainspeksjon", + "object_descriptions": { + "title": "Generative KI-objektbeskrivelser", + "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." + }, + "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." + }, + "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 på {{cameraName}}, vil bli vist som deteksjoner uavhengig av hvilken sone de er i.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert i {{zone}} på {{cameraName}}, vil bli vist som deteksjoner.", + "notSelectDetections": "Alle {{detectionsLabels}}-objekter oppdaget i {{zone}} på {{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 på {{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." + } + } } } diff --git a/web/public/locales/nb-NO/views/system.json b/web/public/locales/nb-NO/views/system.json index 884949bd9..d04cefd93 100644 --- a/web/public/locales/nb-NO/views/system.json +++ b/web/public/locales/nb-NO/views/system.json @@ -40,7 +40,8 @@ "title": "Detektorer", "cpuUsage": "Detektor CPU-belastning", "memoryUsage": "Detektor minnebruk", - "temperature": "Detektor temperatur" + "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", @@ -73,12 +74,24 @@ "title": "Maskinvareinformasjon", "gpuUsage": "GPU-belastning", "npuMemory": "NPU minne", - "npuUsage": "NPU belastning" + "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." + } }, "otherProcesses": { "title": "Andre prosesser", "processCpuUsage": "Prosessenes CPU-belastning", - "processMemoryUsage": "Prosessenes minnebruk" + "processMemoryUsage": "Prosessenes minnebruk", + "series": { + "go2rtc": "go2rtc", + "recording": "opptak", + "review_segment": "inspeksjonselementer", + "embeddings": "vektorrepresentasjoner", + "audio_detector": "lyddetektor" + } } }, "storage": { @@ -100,7 +113,11 @@ "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" + "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." + } }, "cameras": { "info": { @@ -113,7 +130,7 @@ "fetching": "Henter kameradata", "stream": "Strøm {{idx}}", "video": "Video:", - "fps": "Bilder per sekund:", + "fps": "FPS:", "unknown": "Ukjent", "tips": { "title": "Kamerainformasjon" @@ -160,10 +177,20 @@ "text_embedding": "Tekst-vektorrepresentasjoner", "plate_recognition": "Kjennemerke gjenkjenning", "yolov9_plate_detection_speed": "Hastighet for YOLOv9 kjennemerkedeteksjon", - "yolov9_plate_detection": "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" + "infPerSecond": "Inferenser per sekund", + "averageInf": "Gjennomsnittlig inferenstid" }, "title": "System", "metrics": "Systemmålinger", @@ -175,6 +202,7 @@ "reindexingEmbeddings": "Reindeksering av vektorrepresentasjoner ({{processed}}% fullført)", "cameraIsOffline": "{{camera}} er frakoblet", "detectIsSlow": "{{detect}} er treg ({{speed}} ms)", - "detectIsVerySlow": "{{detect}} er veldig treg ({{speed}} ms)" + "detectIsVerySlow": "{{detect}} er veldig treg ({{speed}} ms)", + "shmTooLow": "/dev/shm-allokeringen ({{total}} MB) bør økes til minst {{min}} MB." } } diff --git a/web/public/locales/nl/audio.json b/web/public/locales/nl/audio.json index e99acca9e..59268c7ef 100644 --- a/web/public/locales/nl/audio.json +++ b/web/public/locales/nl/audio.json @@ -16,7 +16,7 @@ "snoring": "Snurken", "gasp": "Snakken naar adem", "pant": "Hijgen", - "snort": "Snorren", + "snort": "Snuiven", "sneeze": "Niezen", "shuffle": "Schudden", "footsteps": "Voetstappen", @@ -425,5 +425,79 @@ "environmental_noise": "Omgevingsgeluid", "silence": "Stilte", "sound_effect": "Geluidseffect", - "scream": "Schreeuw" + "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 index 0af38d8a5..045ce5199 100644 --- a/web/public/locales/nl/common.json +++ b/web/public/locales/nl/common.json @@ -81,7 +81,10 @@ "formattedTimestampMonthDayYear": { "12hour": "d MMM yyyy", "24hour": "d MMM yyyy" - } + }, + "inProgress": "Wordt uitgevoerd", + "invalidStartTime": "Ongeldige starttijd", + "invalidEndTime": "Ongeldige eindtijd" }, "button": { "enabled": "Ingeschakeld", @@ -118,7 +121,8 @@ "download": "Download", "unselect": "Deselecteren", "next": "Volgende", - "deleteNow": "Nu verwijderen" + "deleteNow": "Nu verwijderen", + "continue": "Doorgaan" }, "unit": { "speed": { @@ -128,10 +132,24 @@ "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" + "back": "Ga terug", + "hide": "Verberg {{item}}", + "show": "Toon {{item}}", + "ID": "ID", + "none": "Geen", + "all": "Alle", + "other": "Overige" }, "menu": { "system": "Systeem", @@ -175,7 +193,15 @@ "ja": "日本語 (Japans)", "yue": "粵語 (Kantonees)", "th": "ไทย (Thais)", - "ca": "Català (Catalaans)" + "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)" }, "darkMode": { "label": "Donkere modus", @@ -224,7 +250,8 @@ "setPassword": "Wachtwoord instellen", "account": "Account", "anonymous": "anoniem" - } + }, + "classification": "Classificatie" }, "toast": { "copyUrlToClipboard": "URL naar klembord gekopieerd.", @@ -239,7 +266,7 @@ "role": { "title": "Rol", "admin": "Beheerder", - "viewer": "Gebruiker", + "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": { @@ -264,5 +291,18 @@ "title": "404", "documentTitle": "Niet gevonden - Frigate" }, - "selectItem": "Selecteer {{item}}" + "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" + } } diff --git a/web/public/locales/nl/components/auth.json b/web/public/locales/nl/components/auth.json index 78ae8e55e..14fb57adf 100644 --- a/web/public/locales/nl/components/auth.json +++ b/web/public/locales/nl/components/auth.json @@ -10,6 +10,7 @@ "unknownError": "Onbekende fout. Bekijk de logs.", "webUnknownError": "Onbekende fout. Controleer consolelogboeken." }, - "user": "Gebruikersnaam" + "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 index 251e57a25..1b840478d 100644 --- a/web/public/locales/nl/components/camera.json +++ b/web/public/locales/nl/components/camera.json @@ -65,7 +65,8 @@ "title": "{{cameraName}} Streaming-instellingen", "stream": "Stream", "placeholder": "Kies een stream" - } + }, + "birdseye": "Birdseye" }, "icon": "Icon" }, diff --git a/web/public/locales/nl/components/dialog.json b/web/public/locales/nl/components/dialog.json index 0c1e8aaf3..b666c2b0c 100644 --- a/web/public/locales/nl/components/dialog.json +++ b/web/public/locales/nl/components/dialog.json @@ -12,7 +12,7 @@ "plus": { "submitToPlus": { "label": "Verzenden naar Frigate+", - "desc": "Objecten op locaties die je wilt vermijden, zijn geen valspositieven. Als je ze als valspositieven indient, brengt dit het model in verwarring." + "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": { @@ -42,7 +42,7 @@ }, "export": { "time": { - "fromTimeline": "Selecteer uit tijdlijn", + "fromTimeline": "Selecteer uit Tijdlijn", "end": { "label": "Selecteer eindtijd", "title": "Eindtijd" @@ -65,7 +65,8 @@ "noVaildTimeSelected": "Geen geldig tijdsbereik geselecteerd", "endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn" }, - "success": "Export is succesvol gestart. Bekijk het bestand in de map /exports." + "success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.", + "view": "Weergeven" }, "fromTimeline": { "saveExport": "Export opslaan", @@ -105,9 +106,10 @@ }, "recording": { "button": { - "deleteNow": "Nu verwijderen", + "deleteNow": "Verwijder nu", "export": "Exporteren", - "markAsReviewed": "Markeren als beoordeeld" + "markAsReviewed": "Markeren als beoordeeld", + "markAsUnreviewed": "Markeren als niet beoordeeld" }, "confirmDelete": { "desc": { @@ -119,5 +121,13 @@ "success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd." } } + }, + "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 index fa2ecd9d0..e910acd83 100644 --- a/web/public/locales/nl/components/filter.json +++ b/web/public/locales/nl/components/filter.json @@ -75,7 +75,9 @@ "title": "Herkende kentekenplaten", "noLicensePlatesFound": "Geen kentekenplaten gevonden.", "selectPlatesFromList": "Selecteer een of meer kentekens uit de lijst.", - "loading": "Herkende kentekenplaten laden…" + "loading": "Herkende kentekenplaten laden…", + "selectAll": "Selecteer alles", + "clearAll": "Alles wissen" }, "score": "Score", "sort": { @@ -123,5 +125,17 @@ "label": "Filters resetten naar standaardwaarden" }, "more": "Meer filters", - "estimatedSpeed": "Geschatte snelheid ({{unit}})" + "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/objects.json b/web/public/locales/nl/objects.json index a0b21657b..1fc914a77 100644 --- a/web/public/locales/nl/objects.json +++ b/web/public/locales/nl/objects.json @@ -14,7 +14,7 @@ "traffic_light": "Verkeerslicht", "street_sign": "Verkeersbord", "stop_sign": "Stopbord", - "parking_meter": "Parkeer Meter", + "parking_meter": "Parkeermeter", "bench": "Bankje", "cow": "Koe", "giraffe": "Giraffe", diff --git a/web/public/locales/nl/views/classificationModel.json b/web/public/locales/nl/views/classificationModel.json new file mode 100644 index 000000000..a94c7956b --- /dev/null +++ b/web/public/locales/nl/views/classificationModel.json @@ -0,0 +1,188 @@ +{ + "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": "Verwijderde klasse", + "deletedImage": "Verwijderde 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}}" + }, + "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}}" + } + }, + "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": "Voorbeelden van ontbrekende staten", + "description": "Het wordt aanbevolen om voor alle staten voorbeelden te selecteren voor het beste resultaat. Je kunt doorgaan zonder alle staten te selecteren, maar het model wordt pas getraind zodra alle staten afbeeldingen hebben. Na het doorgaan kun je in de weergave ‘Recente Classificaties’ de ontbrekende staten van afbeeldingen voorzien, en daarna het model trainen." + } + } + }, + "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" +} diff --git a/web/public/locales/nl/views/configEditor.json b/web/public/locales/nl/views/configEditor.json index 5bd94a242..50a146cb6 100644 --- a/web/public/locales/nl/views/configEditor.json +++ b/web/public/locales/nl/views/configEditor.json @@ -12,5 +12,7 @@ }, "configEditor": "Configuratie Bewerken", "saveOnly": "Alleen opslaan", - "confirm": "Afsluiten zonder op te slaan?" + "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 index 269cadffc..b4be69aef 100644 --- a/web/public/locales/nl/views/events.json +++ b/web/public/locales/nl/views/events.json @@ -13,7 +13,11 @@ "empty": { "alert": "Er zijn geen meldingen om te beoordelen", "detection": "Er zijn geen detecties om te beoordelen", - "motion": "Geen bewegingsgegevens gevonden" + "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", @@ -34,5 +38,30 @@ "markTheseItemsAsReviewed": "Markeer deze items als beoordeeld", "selected_other": "{{count}} geselecteerd", "selected_one": "{{count}} geselecteerd", - "detected": "gedetecteerd" + "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 index 78c2c7116..dcef557f0 100644 --- a/web/public/locales/nl/views/explore.json +++ b/web/public/locales/nl/views/explore.json @@ -33,7 +33,9 @@ "details": "Details", "video": "video", "snapshot": "snapshot", - "object_lifecycle": "objectlevenscyclus" + "object_lifecycle": "objectlevenscyclus", + "thumbnail": "thumbnail", + "tracking_details": "trackinggegevens" }, "objectLifecycle": { "createObjectMask": "Objectmasker maken", @@ -102,12 +104,16 @@ "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." + "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}}" + "updatedLPRFailed": "Kentekenplaat bijwerken mislukt: {{errorMessage}}", + "audioTranscription": "Audiotranscriptie aanvragen mislukt: {{errorMessage}}", + "updatedAttributesFailed": "Attributen konden niet worden bijgewerkt: {{errorMessage}}" } } }, @@ -152,7 +158,18 @@ }, "recognizedLicensePlate": "Erkende kentekenplaat", "snapshotScore": { - "label": "Snapshot scoren" + "label": "Snapshot score" + }, + "score": { + "label": "Score" + }, + "editAttributes": { + "title": "Bewerk attributen", + "desc": "Selecteer classificatiekenmerken voor dit {{label}}" + }, + "attributes": "Classificatie-kenmerken", + "title": { + "label": "Titel" } }, "itemMenu": { @@ -182,6 +199,28 @@ "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" } }, "noTrackedObjects": "Geen gevolgde objecten gevonden", @@ -194,14 +233,71 @@ "error": "Verwijderen van gevolgd object mislukt: {{errorMessage}}" } }, - "tooltip": "{{type}} komt voor {{confidence}}% overeen met de zoekopdracht" + "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 levenscyclusgegevens van het object. Opgenomen videobeelden van dit object in de Geschiedenisweergave worden NIET verwijderd.

    Weet je zeker dat je wilt doorgaan?" + "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?" } }, "fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}", - "exploreMore": "Verken meer {{label}} objecten" + "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" + } + }, + "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 index 2589f37c7..b4223a612 100644 --- a/web/public/locales/nl/views/exports.json +++ b/web/public/locales/nl/views/exports.json @@ -13,5 +13,11 @@ }, "noExports": "Geen export gevonden", "deleteExport": "Verwijder Export", - "deleteExport.desc": "Weet je zeker dat je dit wilt wissen: {{exportName}}?" + "deleteExport.desc": "Weet je zeker dat je dit wilt wissen: {{exportName}}?", + "tooltip": { + "shareExport": "Deel export", + "downloadVideo": "Download video", + "editName": "Naam bewerken", + "deleteExport": "Verwijder export" + } } diff --git a/web/public/locales/nl/views/faceLibrary.json b/web/public/locales/nl/views/faceLibrary.json index ecc636fda..88ce52e0f 100644 --- a/web/public/locales/nl/views/faceLibrary.json +++ b/web/public/locales/nl/views/faceLibrary.json @@ -13,13 +13,14 @@ "documentTitle": "Gezichtsbibliotheek - Frigate", "description": { "placeholder": "Voer een naam in voor deze verzameling", - "addFace": "Doorloop het toevoegen van een nieuwe collectie aan de gezichtenbibliotheek.", + "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." }, "train": { - "title": "Train", - "aria": "Selecteer trainen", - "empty": "Er zijn geen recente pogingen tot gezichtsherkenning" + "title": "Recente herkenningen", + "aria": "Selecteer recente herkenningen", + "empty": "Er zijn geen recente pogingen tot gezichtsherkenning", + "titleShort": "Recent" }, "selectFace": "Selecteer gezicht", "toast": { @@ -36,7 +37,7 @@ "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.", + "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.", @@ -46,7 +47,7 @@ }, "imageEntry": { "dropActive": "Zet de afbeelding hier neer…", - "dropInstructions": "Sleep een afbeelding hierheen of klik om te selecteren", + "dropInstructions": "Sleep een afbeelding hierheen, of klik om te selecteren", "maxSize": "Maximale grootte: {{size}}MB", "validation": { "selectImage": "Selecteer een afbeeldingbestand." @@ -56,7 +57,7 @@ "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 Trainen om per gedetecteerd persoon afbeeldingen te selecteren en te trainen.
  • Richt je op frontale afbeeldingen voor het beste resultaat; vermijd trainingsbeelden waarop gezichten vanuit een hoek te zien zijn.
  • " + "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", diff --git a/web/public/locales/nl/views/live.json b/web/public/locales/nl/views/live.json index d09f4c699..e6dd73bea 100644 --- a/web/public/locales/nl/views/live.json +++ b/web/public/locales/nl/views/live.json @@ -41,7 +41,15 @@ "label": "Klik in het frame om de PTZ-camera te centreren" } }, - "presets": "PTZ-camerapresets" + "presets": "PTZ-camerapresets", + "focus": { + "in": { + "label": "Focus PTZ-camera in" + }, + "out": { + "label": "Focus PTZ-camera uit" + } + } }, "camera": { "enable": "Camera inschakelen", @@ -83,8 +91,8 @@ "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": "Opname op aanvraag", - "tips": "Start een handmatige gebeurtenis op basis van de opnamebehoudinstellingen van deze camera.", + "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", @@ -120,12 +128,15 @@ "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 stroom", + "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": { @@ -135,7 +146,8 @@ "audioDetection": "Audiodetectie", "autotracking": "Automatisch volgen", "snapshots": "Momentopnames", - "cameraEnabled": "Camera ingeschakeld" + "cameraEnabled": "Camera ingeschakeld", + "transcription": "Audiotranscriptie" }, "history": { "label": "Historische beelden weergeven" @@ -154,5 +166,24 @@ "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." + } } } diff --git a/web/public/locales/nl/views/search.json b/web/public/locales/nl/views/search.json index 47487be38..7552d1439 100644 --- a/web/public/locales/nl/views/search.json +++ b/web/public/locales/nl/views/search.json @@ -26,7 +26,8 @@ "search_type": "Zoektype", "zones": "Zones", "max_speed": "Max snelheid", - "after": "Na" + "after": "Na", + "attributes": "Kenmerken" }, "toast": { "error": { diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json index 5189f57bf..dade19f15 100644 --- a/web/public/locales/nl/views/settings.json +++ b/web/public/locales/nl/views/settings.json @@ -7,10 +7,12 @@ "classification": "Classificatie-instellingen - Frigate", "masksAndZones": "Masker- en zone-editor - Frigate", "object": "Foutopsporing Frigate", - "general": "Algemene instellingen - Frigate", + "general": "Gebruikersinterface-instellingen - Frigate", "frigatePlus": "Frigate+ Instellingen - Frigate", "notifications": "Meldingsinstellingen - Frigate", - "enrichments": "Verrijkingsinstellingen - Frigate" + "enrichments": "Verrijkingsinstellingen - Frigate", + "cameraManagement": "Camera's beheren - Frigate", + "cameraReview": "Camera Review Instellingen - Frigate" }, "menu": { "ui": "Gebruikersinterface", @@ -22,7 +24,11 @@ "notifications": "Meldingen", "cameras": "Camera-instellingen", "frigateplus": "Frigate+", - "enrichments": "Verrijkingen" + "enrichments": "Verrijkingen", + "triggers": "Triggers", + "roles": "Rollen", + "cameraManagement": "Beheer", + "cameraReview": "Beoordeel" }, "dialog": { "unsavedChanges": { @@ -44,9 +50,17 @@ "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": "Algemene instellingen", + "title": "Gebruikersinterface instellingen", "storedLayouts": { "title": "Opgeslagen indelingen", "clearAll": "Alle indelingen wissen", @@ -58,7 +72,7 @@ "clearAll": "Alle streaminginstellingen wissen" }, "recordingsViewer": { - "title": "Opnamebekijker", + "title": "Opnameweergave", "defaultPlaybackRate": { "label": "Standaard afspeelsnelheid", "desc": "Standaard afspeelsnelheid voor het afspelen van opnames." @@ -178,7 +192,45 @@ "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" + "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": { @@ -199,7 +251,8 @@ "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." + "alreadyExists": "Er bestaat al een zone met deze naam voor deze camera.", + "mustHaveAtLeastOneLetter": "De zonenaam moet minimaal één letter bevatten." } }, "distance": { @@ -253,7 +306,7 @@ "name": { "title": "Naam", "inputPlaceHolder": "Voer een naam in…", - "tips": "De naam moet minimaal 2 tekens lang zijn en mag niet gelijk zijn aan de naam van een camera of een andere zone." + "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", @@ -292,7 +345,7 @@ "add": "Zone toevoegen", "allObjects": "Alle objecten", "toast": { - "success": "Zone ({{zoneName}}) is opgeslagen. Start Frigate opnieuw om de wijzigingen toe te passen." + "success": "Zone ({{zoneName}}) is opgeslagen." } }, "motionMasks": { @@ -317,8 +370,8 @@ "point_other": "{{count}} punten", "toast": { "success": { - "title": "{{polygonName}} is opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", - "noName": "Bewegingsmasker is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + "title": "{{polygonName}} is opgeslagen.", + "noName": "Bewegingsmasker is opgeslagen." } }, "add": "Nieuw bewegingsmasker" @@ -338,8 +391,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} is opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", - "noName": "Objectmasker is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + "title": "{{polygonName}} is opgeslagen.", + "noName": "Objectmasker is opgeslagen." } }, "point_one": "{{count}} punt", @@ -420,7 +473,20 @@ "score": "Score", "ratio": "Verhouding" }, - "detectorDesc": "Frigate gebruikt je detectoren ({{detectors}}) om objecten in de videostream van je camera te detecteren." + "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", @@ -429,7 +495,7 @@ "title": "Gebruikersbeheer" }, "addUser": "Gebruiker toevoegen", - "updatePassword": "Wachtwoord bijwerken", + "updatePassword": "Wachtwoord opnieuw instellen", "toast": { "success": { "createUser": "Gebruiker {{user}} succesvol aangemaakt", @@ -449,7 +515,7 @@ "role": "Rol", "noUsers": "Geen gebruikers gevonden.", "changeRole": "Gebruikersrol wijzigen", - "password": "Wachtwoord", + "password": "Wachtwoord opnieuw instellen", "deleteUser": "Verwijder gebruiker", "username": "Gebruikersnaam" }, @@ -475,7 +541,16 @@ "placeholder": "Wachtwoord bevestigen" }, "placeholder": "Wachtwoord invoeren", - "notMatch": "Wachtwoorden komen niet overeen" + "notMatch": "Wachtwoorden komen niet overeen", + "show": "Wachtwoord weergeven", + "hide": "Wachtwoord verbergen", + "requirements": { + "title": "Wachtwoordvereisten:", + "length": "Minimaal 8 tekens", + "uppercase": "Minimaal één hoofdletter", + "digit": "Minimaal één cijfer", + "special": "Minimaal één speciaal teken (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "Nieuw wachtwoord", @@ -485,7 +560,11 @@ } }, "usernameIsRequired": "Gebruikersnaam is vereist", - "passwordIsRequired": "Wachtwoord is vereist" + "passwordIsRequired": "Wachtwoord is vereist", + "currentPassword": { + "title": "Huidig wachtwoord", + "placeholder": "Voer uw huidige wachtwoord in" + } }, "createUser": { "title": "Nieuwe gebruiker aanmaken", @@ -505,8 +584,9 @@ "intro": "Selecteer een gepaste rol voor deze gebruiker:", "admin": "Beheerder", "adminDesc": "Volledige toegang tot alle functies.", - "viewer": "Gebruiker", - "viewerDesc": "Alleen toegang tot Live-dashboards, Beoordelen, Verkennen en Exports." + "viewer": "Kijker", + "viewerDesc": "Alleen toegang tot Live-dashboards, Beoordelen, Verkennen en Exports.", + "customDesc": "Aangepaste rol met specifieke cameratoegang." }, "select": "Selecteer een rol" }, @@ -515,7 +595,12 @@ "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" + "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." } } }, @@ -680,5 +765,546 @@ "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." + } + } + } + }, + "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." + }, + "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" + } + } + }, + "cameraReview": { + "title": "Camerabeoordelings-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." + }, + "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." + } + } } } diff --git a/web/public/locales/nl/views/system.json b/web/public/locales/nl/views/system.json index 7d039d08e..73ba194d0 100644 --- a/web/public/locales/nl/views/system.json +++ b/web/public/locales/nl/views/system.json @@ -11,7 +11,7 @@ "enrichments": "Verrijkings Statistieken - Frigate" }, "title": "Systeem", - "metrics": "Systeem statistieken", + "metrics": "Systeemstatistieken", "logs": { "download": { "label": "Logs Downloaden" @@ -41,10 +41,11 @@ "cpuUsage": "Detector CPU-verbruik", "memoryUsage": "Detector Geheugen Gebruik", "inferenceSpeed": "Detector Interferentie Snelheid", - "temperature": "Detectortemperatuur" + "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": "Systeem Gegevens", + "title": "Systeemgegevens", "gpuUsage": "GPU-verbruik", "gpuInfo": { "vainfoOutput": { @@ -74,12 +75,24 @@ "gpuEncoder": "GPU Encodeerder", "gpuMemory": "GPU-geheugen", "npuUsage": "NPU-gebruik", - "npuMemory": "NPU-geheugen" + "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." + } }, "otherProcesses": { "processMemoryUsage": "Process Geheugen Gebruik", "processCpuUsage": "Process CPU-verbruik", - "title": "Verdere Processen" + "title": "Verdere Processen", + "series": { + "go2rtc": "go2rtc", + "recording": "opname", + "review_segment": "beoordelingssegment", + "embeddings": "inbeddingen", + "audio_detector": "Geluidsdetector" + } }, "title": "Algemeen" }, @@ -102,7 +115,12 @@ "camera": "Camera", "bandwidth": "Bandbreedte" }, - "title": "Opslag" + "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", @@ -154,11 +172,12 @@ "stats": { "ffmpegHighCpuUsage": "{{camera}} zorgt voor hoge FFmpeg CPU belasting ({{ffmpegAvg}}%)", "detectHighCpuUsage": "{{camera}} zorgt voor hoge detectie CPU belasting ({{detectAvg}}%)", - "healthy": "Systeem is gezond", + "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" + "cameraIsOffline": "{{camera}} is offline", + "shmTooLow": "Vergroot de /dev/shm toewijzing van {{total}} MB naar minimaal {{min}} MB." }, "enrichments": { "title": "Verrijkingen", @@ -174,7 +193,17 @@ "face_recognition": "Gezichtsherkenning", "yolov9_plate_detection_speed": "YOLOv9 Kentekenplaat Detectiesnelheid", "yolov9_plate_detection": "YOLOv9 Kentekenplaatdetectie", - "plate_recognition": "Kentekenherkenning" - } + "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/peo/audio.json b/web/public/locales/peo/audio.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/common.json b/web/public/locales/peo/common.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/auth.json b/web/public/locales/peo/components/auth.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/camera.json b/web/public/locales/peo/components/camera.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/dialog.json b/web/public/locales/peo/components/dialog.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/filter.json b/web/public/locales/peo/components/filter.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/icons.json b/web/public/locales/peo/components/icons.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/input.json b/web/public/locales/peo/components/input.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/components/player.json b/web/public/locales/peo/components/player.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/objects.json b/web/public/locales/peo/objects.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/classificationModel.json b/web/public/locales/peo/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/configEditor.json b/web/public/locales/peo/views/configEditor.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/events.json b/web/public/locales/peo/views/events.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/explore.json b/web/public/locales/peo/views/explore.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/exports.json b/web/public/locales/peo/views/exports.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/faceLibrary.json b/web/public/locales/peo/views/faceLibrary.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/live.json b/web/public/locales/peo/views/live.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/recording.json b/web/public/locales/peo/views/recording.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/search.json b/web/public/locales/peo/views/search.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/settings.json b/web/public/locales/peo/views/settings.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/peo/views/system.json b/web/public/locales/peo/views/system.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/peo/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pl/audio.json b/web/public/locales/pl/audio.json index 62cd7b465..4d8e1f28d 100644 --- a/web/public/locales/pl/audio.json +++ b/web/public/locales/pl/audio.json @@ -124,7 +124,7 @@ "zither": "Cytra", "ukulele": "Ukulele", "keyboard": "Klawiatura", - "rimshot": "Rimshot", + "rimshot": "Uderzenie w obręcz", "drum_roll": "Werbel (tremolo)", "bass_drum": "Bęben basowy", "timpani": "Kotły", @@ -168,14 +168,14 @@ "didgeridoo": "Didgeridoo", "theremin": "Theremin", "singing_bowl": "Misa dźwiękowa", - "scratching": "Scratching", + "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": "Grunge", + "grunge": "Paskudztwo", "progressive_rock": "Rock progresywny", "rock_and_roll": "Rock and roll", "psychedelic_rock": "Rock psychodeliczny", @@ -425,5 +425,79 @@ "pulleys": "Bloczki", "sanding": "Szlifowanie", "clock": "Zegar", - "tick": "Tykanie" + "tick": "Tykanie", + "sodeling": "Sodeling", + "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": "Child", + "change_ringing": "Zmienny dzwonek", + "shofar": "Shofar", + "trickle": "Trickle", + "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": "Sizzle", + "clickety_clack": "Klik-klak", + "rumble": "Grzmot", + "plop": "Plop", + "hum": "Szum", + "zing": "Zing", + "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 index 00f14d246..dbf1576a0 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -22,18 +22,18 @@ "yesterday": "Wczoraj", "pm": "po południu", "am": "przed południem", - "yr": "{{time}}r.", + "yr": "{{time}}r", "year_one": "{{time}} rok", "year_few": "{{time}} lata", "year_many": "{{time}} lat", - "mo": "{{time}}m.", - "d": "{{time}}d.", + "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.", + "h": "{{time}}godz", + "m": "{{time}}min", + "s": "{{time}}s", "month_one": "{{time}} miesiąc", "month_few": "{{time}} miesiące", "month_many": "{{time}} miesięcy", @@ -87,7 +87,10 @@ "formattedTimestampMonthDayYear": { "12hour": "d MMMM yyyy", "24hour": "d MMMM yyyy" - } + }, + "inProgress": "W trakcie", + "invalidStartTime": "Nieprawidłowy czas rozpoczęcia", + "invalidEndTime": "Nieprawidłowy czas zakończenia" }, "unit": { "speed": { @@ -97,10 +100,23 @@ "length": { "feet": "stopy", "meters": "metry" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/godz", + "mbph": "MB/godz", + "gbph": "GB/godz" } }, "label": { - "back": "Wróć" + "back": "Wróć", + "hide": "Ukryj {{item}}", + "show": "Pokaż {{item}}", + "ID": "ID", + "none": "Brak", + "all": "Wszystko" }, "button": { "apply": "Zastosuj", @@ -137,7 +153,8 @@ "cameraAudio": "Dźwięk kamery", "off": "WYŁĄCZ", "edit": "Edytuj", - "copyCoordinates": "Kopiuj współrzędne" + "copyCoordinates": "Kopiuj współrzędne", + "continue": "Kontynuuj" }, "menu": { "system": "System", @@ -179,7 +196,15 @@ "fi": "Suomi (Fiński)", "yue": "粵語 (Kantoński)", "th": "ไทย (Tajski)", - "ca": "Català (Kataloński)" + "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)" }, "appearance": "Wygląd", "darkMode": { @@ -231,7 +256,8 @@ "configurationEditor": "Edytor konfiguracji", "help": "Pomoc", "settings": "Ustawienia", - "export": "Eksportuj" + "export": "Eksportuj", + "classification": "Klasyfikacja" }, "role": { "viewer": "Przeglądający", @@ -271,5 +297,18 @@ }, "title": "Zapisz" } + }, + "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" } } diff --git a/web/public/locales/pl/components/auth.json b/web/public/locales/pl/components/auth.json index 094e0ca97..12aba0fb6 100644 --- a/web/public/locales/pl/components/auth.json +++ b/web/public/locales/pl/components/auth.json @@ -10,6 +10,7 @@ "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 index afeb414d2..f67326172 100644 --- a/web/public/locales/pl/components/camera.json +++ b/web/public/locales/pl/components/camera.json @@ -66,7 +66,8 @@ }, "placeholder": "Wybierz strumień", "stream": "Strumień" - } + }, + "birdseye": "Widok z lotu ptaka" } }, "debug": { diff --git a/web/public/locales/pl/components/dialog.json b/web/public/locales/pl/components/dialog.json index 49d1764c3..24842e140 100644 --- a/web/public/locales/pl/components/dialog.json +++ b/web/public/locales/pl/components/dialog.json @@ -65,12 +65,13 @@ "export": "Eksportuj", "selectOrExport": "Wybierz lub Eksportuj", "toast": { - "success": "Pomyślnie rozpoczęto eksport. Zobacz plik w folderze /exports.", + "success": "Pomyślnie rozpoczęto eksport. Zobacz plik na stronie eksportów.", "error": { "failed": "Nie udało się rozpocząć eksportu: {{error}}", "endTimeMustAfterStartTime": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia", "noVaildTimeSelected": "Nie wybrano prawidłowego zakresu czasu" - } + }, + "view": "Widok" }, "fromTimeline": { "saveExport": "Zapisz Eksport", @@ -81,7 +82,8 @@ "button": { "markAsReviewed": "Oznacz jako sprawdzone", "deleteNow": "Usuń teraz", - "export": "Eksportuj" + "export": "Eksportuj", + "markAsUnreviewed": "Oznacz jako niesprawdzone" }, "confirmDelete": { "title": "Potwierdź Usunięcie", @@ -122,5 +124,13 @@ } } } + }, + "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 index 30b19bee3..7de30b2dd 100644 --- a/web/public/locales/pl/components/filter.json +++ b/web/public/locales/pl/components/filter.json @@ -7,7 +7,7 @@ "short": "Etykiety" }, "count_one": "{{count}} Etykieta", - "count_other": "{{count}} Etykiet" + "count_other": "{{count}} Etykiet(y)" }, "zones": { "label": "Strefy", @@ -85,7 +85,9 @@ "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." + "selectPlatesFromList": "Wybierz jedną lub więcej tablic z listy.", + "selectAll": "Wybierz wszystko", + "clearAll": "Wyczyść wszystko" }, "dates": { "all": { @@ -122,5 +124,17 @@ }, "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/views/classificationModel.json b/web/public/locales/pl/views/classificationModel.json new file mode 100644 index 000000000..c68baf133 --- /dev/null +++ b/web/public/locales/pl/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "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": "Usunięte klasy", + "deletedImage": "Usunięte obrazy", + "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}}" + }, + "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}}" + } + }, + "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." + } + } + } +} diff --git a/web/public/locales/pl/views/configEditor.json b/web/public/locales/pl/views/configEditor.json index 2ebc8c613..a8c374044 100644 --- a/web/public/locales/pl/views/configEditor.json +++ b/web/public/locales/pl/views/configEditor.json @@ -12,5 +12,7 @@ }, "saveOnly": "Tylko zapisz", "saveAndRestart": "Zapisz i uruchom ponownie", - "confirm": "Zamknąć bez zapisu?" + "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 index cf53b56e0..0ffc5419f 100644 --- a/web/public/locales/pl/views/events.json +++ b/web/public/locales/pl/views/events.json @@ -10,7 +10,11 @@ "empty": { "alert": "Brak alertów do przejrzenia", "detection": "Brak detekcji do przejrzenia", - "motion": "Nie znaleziono danych o ruchu" + "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": "Oś czasu", "timeline.aria": "Wybierz oś czasu", @@ -34,5 +38,30 @@ }, "selected_one": "{{count}} wybrane", "selected_other": "{{count}} wybrane", - "detected": "wykryto" + "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 index cd0c1048f..d18d065b8 100644 --- a/web/public/locales/pl/views/explore.json +++ b/web/public/locales/pl/views/explore.json @@ -20,12 +20,16 @@ "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ą." + "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}}" + "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}}" } } }, @@ -70,6 +74,17 @@ "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ł" } }, "objectLifecycle": { @@ -154,7 +169,9 @@ "details": "szczegóły", "snapshot": "zrzut ekranu", "video": "wideo", - "object_lifecycle": "cykl życia obiektu" + "object_lifecycle": "cykl życia obiektu", + "thumbnail": "miniaturka", + "tracking_details": "szczegóły śledzenia" }, "itemMenu": { "downloadSnapshot": { @@ -183,6 +200,28 @@ }, "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" } }, "trackedObjectsCount_one": "{{count}} śledzony obiekt ", @@ -191,7 +230,7 @@ "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 cyklu życia obiektu. Nagrany materiał tego śledzonego obiektu w widoku Historii NIE zostanie usunięty.

    Czy na pewno chcesz kontynuować?", + "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" } }, @@ -203,7 +242,64 @@ "error": "Nie udało się usunąć śledzonego obiektu: {{errorMessage}}" } }, - "tooltip": "Pasuje do {{type}} z pewnością {{confidence}}%" + "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}}" + "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" + }, + "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 index 954849a1a..b0d41bbc3 100644 --- a/web/public/locales/pl/views/exports.json +++ b/web/public/locales/pl/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Nie udało się zmienić nazwy eksportu: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Udostępnij eksport", + "downloadVideo": "Pobierz wideo", + "editName": "Edytuj nazwę", + "deleteExport": "Usuń eksport" } } diff --git a/web/public/locales/pl/views/faceLibrary.json b/web/public/locales/pl/views/faceLibrary.json index a10105faa..4bd3944f8 100644 --- a/web/public/locales/pl/views/faceLibrary.json +++ b/web/public/locales/pl/views/faceLibrary.json @@ -1,9 +1,9 @@ { "selectItem": "Wybierz {{item}}", "description": { - "addFace": "Poznaj proces dodawania nowej kolekcji do biblioteki twarzy.", + "addFace": "Dodaj nową kolekcję do biblioteki twarzy, przesyłając swoje pierwsze zdjęcie.", "placeholder": "Wprowadź nazwę tej kolekcji", - "invalidName": "Nieprawidłowa nazwa. Nazwy mogą zawierać tylko litery, cyfry, spacje, apostrofy, podkreślenia oraz myślniki." + "invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki." }, "details": { "person": "Osoba", @@ -24,12 +24,13 @@ "title": "Utwórz kolekcję", "desc": "Utwórz nową kolekcję", "new": "Utwórz nową twarz", - "nextSteps": "Aby zbudować solidną podstawę:
  • Użyj zakładki Trenuj, 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.
  • " + "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 trenowanie", - "title": "Trenuj", - "empty": "Nie podjęto ostatnio żadnych prób rozpoznawania twarzy" + "aria": "Wybierz ostatnio rozpoznane", + "title": "Ostatnie rozpoznania", + "empty": "Nie podjęto ostatnio żadnych prób rozpoznawania twarzy", + "titleShort": "Ostatnie" }, "selectFace": "Wybierz twarz", "deleteFaceLibrary": { @@ -63,7 +64,7 @@ "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.", + "updatedFaceScore": "Pomyślnie zaktualizowano wynik twarzy do {{name}} {{score}}.", "renamedFace": "Pomyślnie zmieniono nazwę twarzy na {{name}}" }, "error": { diff --git a/web/public/locales/pl/views/live.json b/web/public/locales/pl/views/live.json index 87b0af4ab..417354f63 100644 --- a/web/public/locales/pl/views/live.json +++ b/web/public/locales/pl/views/live.json @@ -43,7 +43,15 @@ "label": "Kliknij w ramce, aby wyśrodkować kamerę PTZ" } }, - "presets": "Presety kamery PTZ" + "presets": "Presety kamery PTZ", + "focus": { + "in": { + "label": "Zmniejsz ostrość kamery PTZ" + }, + "out": { + "label": "Wyostrz kamerę PTZ" + } + } }, "recording": { "enable": "Włącz nagrywanie", @@ -59,7 +67,7 @@ }, "manualRecording": { "title": "Nagrywanie na żądanie", - "tips": "Rozpocznij ręczne zdarzenie w oparciu o ustawienia przechowywania nagrań tej kamery.", + "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." @@ -105,6 +113,9 @@ "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": { @@ -114,7 +125,8 @@ "recording": "Nagrywanie", "snapshots": "Zrzuty ekranu", "audioDetection": "Wykrywanie dźwięku", - "autotracking": "Automatyczne śledzenie" + "autotracking": "Automatyczne śledzenie", + "transcription": "Stenogram" }, "effectiveRetainMode": { "modes": { @@ -154,5 +166,24 @@ "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." + } + }, + "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/search.json b/web/public/locales/pl/views/search.json index 175b42a80..9de364f59 100644 --- a/web/public/locales/pl/views/search.json +++ b/web/public/locales/pl/views/search.json @@ -26,7 +26,8 @@ "after": "Po", "search_type": "Typ wyszukiwania", "time_range": "Zakres czasu", - "before": "Przed" + "before": "Przed", + "attributes": "Właściwości" }, "searchType": { "thumbnail": "Miniatura", diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json index fd45430f3..dfc74b7dc 100644 --- a/web/public/locales/pl/views/settings.json +++ b/web/public/locales/pl/views/settings.json @@ -9,7 +9,11 @@ "masksAndZones": "Maski / Strefy", "motionTuner": "Konfigurator Ruchu", "debug": "Debugowanie", - "enrichments": "Wzbogacenia" + "enrichments": "Wzbogacenia", + "triggers": "Wyzwalacze", + "roles": "Role", + "cameraManagement": "Zarządzanie", + "cameraReview": "Przegląd" }, "dialog": { "unsavedChanges": { @@ -22,7 +26,7 @@ "noCamera": "Brak Kamery" }, "general": { - "title": "Ustawienia Ogólne", + "title": "Ustawienia interfejsu użytkownika", "storedLayouts": { "title": "Zapisane Układy", "clearAll": "Wyczyść Wszystkie Układy", @@ -46,6 +50,14 @@ "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": { @@ -77,12 +89,14 @@ "masksAndZones": "Edytor Masek i Stref - Frigate", "frigatePlus": "Ustawienia Frigate+ - Frigate", "classification": "Ustawienia Klasyfikacji - Frigate", - "general": "Ustawienia Ogólne - Frigate", + "general": "Ustawienia Interfejsu - Frigate", "authentication": "Ustawienia Uwierzytelniania - Frigate", "motionTuner": "Konfigurator Ruchu - Frigate", "object": "Debug - Frigate", "notifications": "Ustawienia powiadomień - Frigate", - "enrichments": "Ustawienia wzbogacania - Frigate" + "enrichments": "Ustawienia wzbogacania - Frigate", + "cameraManagement": "Zarządzanie kamerami – Frigate", + "cameraReview": "Ustawienia przeglądu kamer - Frigate" }, "classification": { "title": "Ustawienia Klasyfikacji", @@ -173,11 +187,48 @@ "alerts": "Alerty ", "title": "Przegląd", "detections": "Wykrycia ", - "desc": "Włącz/wyłącz alerty i wykrywania dla tej kamery. Po wyłączeniu nie będą generowane nowe elementy do przeglądu." + "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": "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.", + "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": { @@ -191,7 +242,8 @@ "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." + "mustNotContainPeriod": "Nazwa strefy nie może zawierać kropki.", + "mustHaveAtLeastOneLetter": "Nazwa strefy musi zawierać co najmniej jedną literę." } }, "distance": { @@ -247,7 +299,7 @@ "name": { "title": "Nazwa", "inputPlaceHolder": "Wprowadź nazwę…", - "tips": "Nazwa musi mieć co najmniej 2 znaki i nie może być taka sama jak nazwa kamery lub innej strefy." + "tips": "Nazwa musi mieć co najmniej 1 znak i nie może być taka sama jak nazwa kamery lub innej strefy." }, "objects": { "title": "Obiekty", @@ -285,7 +337,7 @@ "lineDDistance": "Odległość linii D ({{unit}})" }, "toast": { - "success": "Strefa ({{zoneName}}) została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + "success": "Strefa ({{zoneName}}) została zapisana." } }, "motionMasks": { @@ -306,8 +358,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} został zapisany. Uruchom ponownie Frigate, aby zastosować zmiany.", - "noName": "Maska Ruchu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + "title": "{{polygonName}} został zapisany.", + "noName": "Maska Ruchu została zapisana." } }, "label": "Maska ruchu", @@ -320,8 +372,8 @@ "objectMasks": { "toast": { "success": { - "title": "{{polygonName}} został zapisany. Uruchom ponownie Frigate aby wprowadzić zmiany.", - "noName": "Maska Obiektu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + "title": "{{polygonName}} został zapisany.", + "noName": "Maska Obiektu została zapisana." } }, "objects": { @@ -400,6 +452,19 @@ "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": { @@ -427,7 +492,7 @@ }, "users": { "addUser": "Dodaj Użytkownika", - "updatePassword": "Aktualizuj Hasło", + "updatePassword": "Resetuj Hasło", "toast": { "success": { "createUser": "Użytkownik {{user}} został utworzony pomyślnie", @@ -448,7 +513,7 @@ "role": "Rola", "noUsers": "Nie znaleziono użytkowników.", "changeRole": "Zmień rolę użytkownika", - "password": "Hasło", + "password": "Resetuj hasło", "deleteUser": "Usuń użytkownika" }, "dialog": { @@ -473,7 +538,16 @@ }, "title": "Hasło", "placeholder": "Wprowadź hasło", - "notMatch": "Hasła nie pasują" + "notMatch": "Hasła nie pasują", + "show": "Pokaż hasło", + "hide": "Ukryj hasło", + "requirements": { + "title": "Wymagania hasła:", + "length": "Co najmniej 8 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", @@ -483,7 +557,11 @@ } }, "usernameIsRequired": "Nazwa użytkownika jest wymagana", - "passwordIsRequired": "Hasło jest wymagane" + "passwordIsRequired": "Hasło jest wymagane", + "currentPassword": { + "title": "Aktualne hasło", + "placeholder": "Wprowadź aktualne hasło" + } }, "changeRole": { "desc": "Aktualizuj uprawnienia dla {{username}}", @@ -492,7 +570,8 @@ "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" + "viewer": "Przeglądający", + "customDesc": "Własna rola z dedykowanym dostępem do kamery." }, "title": "Zmień rolę użytkownika", "select": "Wybierz role" @@ -513,7 +592,12 @@ "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" + "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": { @@ -640,7 +724,7 @@ } } }, - "title": "Ustawienia wzbogacania", + "title": "Ustawienia wzbogaceń", "unsavedChanges": "Niezapisane zmiany ustawień wzbogacania", "birdClassification": { "title": "Klasyfikacja ptaków", @@ -683,5 +767,460 @@ "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ów przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer.", + "userRolesUpdated_few": "", + "userRolesUpdated_many": "" + }, + "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." + } } } diff --git a/web/public/locales/pl/views/system.json b/web/public/locales/pl/views/system.json index 1d3003fac..ebcc11463 100644 --- a/web/public/locales/pl/views/system.json +++ b/web/public/locales/pl/views/system.json @@ -42,7 +42,12 @@ "gpuMemory": "Pamięć GPU", "gpuUsage": "Użycie GPU", "npuUsage": "Użycie NPU", - "npuMemory": "Pamięć 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": { @@ -50,18 +55,26 @@ "inferenceSpeed": "Szybkość wnioskowania detektora", "cpuUsage": "Użycie CPU przez detektor", "memoryUsage": "Użycie pamięci przez detektor", - "temperature": "Temperatura detektora" + "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" + "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 sondowaniu kamery", + "cameraProbeInfo": "{{camera}} Informacje o ustawieniach kamery", "streamDataFromFFPROBE": "Dane strumienia są pozyskiwane za pomocą ffprobe.", "video": "Wideo:", "codec": "Kodek:", @@ -112,17 +125,21 @@ }, "title": "Magazyn kamery", "camera": "Kamera", - "storageUsed": "Wykorzystany magazyn", + "storageUsed": "Wykorzystana przestrzeń", "percentageOfTotalUsed": "Procent całości", "bandwidth": "Przepustowość", "unusedStorageInformation": "Informacja o niewykorzystanym magazynie" }, - "title": "Magazyn", + "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": { @@ -158,7 +175,8 @@ "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" + "cameraIsOffline": "{{camera}} jest niedostępna", + "shmTooLow": "przydział {{total}} MB dla /dev/shm powinien zostać zwiększony do przynajmniej {{min}} MB." }, "enrichments": { "title": "Wzbogacenia", @@ -174,7 +192,17 @@ "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" - } + "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 index 04ee37d6b..b36f09902 100644 --- a/web/public/locales/pt-BR/audio.json +++ b/web/public/locales/pt-BR/audio.json @@ -1,7 +1,7 @@ { "mantra": "Mantra", "child_singing": "Criança cantando", - "speech": "Discurso", + "speech": "Fala", "yell": "Gritar", "chant": "Canto", "babbling": "Balbuciando", @@ -425,5 +425,9 @@ "artillery_fire": "Fogo de Artilharia", "cap_gun": "Espoleta", "fireworks": "Fogos de Artifício", - "firecracker": "Rojões" + "firecracker": "Rojões", + "noise": "Ruído", + "distortion": "Distorção", + "cacophony": "Cacofonia", + "vibration": "Vibração" } diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json index c5b789ccc..e1ab1e525 100644 --- a/web/public/locales/pt-BR/common.json +++ b/web/public/locales/pt-BR/common.json @@ -3,7 +3,7 @@ "untilForTime": "Até {{time}}", "untilForRestart": "Até o Frigate reiniciar.", "untilRestart": "Até reiniciar", - "ago": "{{timeAgo}} antes", + "ago": "{{timeAgo}} atrás", "justNow": "Agora mesmo", "today": "Hoje", "yesterday": "Ontem", @@ -80,7 +80,7 @@ "24hour": "dd-MM-yy-HH-mm-ss" } }, - "selectItem": "Selecione {{item}}", + "selectItem": "Selecionar {{item}}", "unit": { "speed": { "mph": "mi/h", @@ -89,6 +89,14 @@ "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": { @@ -169,7 +177,15 @@ "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)" }, "systemLogs": "Logs de sistema", "settings": "Configurações", @@ -210,7 +226,7 @@ "count_other": "{{count}} Câmeras" } }, - "review": "Revisão", + "review": "Revisar", "explore": "Explorar", "export": "Exportar", "uiPlayground": "Playground da UI", @@ -261,5 +277,9 @@ "documentTitle": "Não Encontrado - Frigate", "title": "404", "desc": "Página não encontrada" + }, + "readTheDocumentation": "Leia a documentação", + "information": { + "pixels": "{{area}}px" } } diff --git a/web/public/locales/pt-BR/components/auth.json b/web/public/locales/pt-BR/components/auth.json index 7172acaae..27775812f 100644 --- a/web/public/locales/pt-BR/components/auth.json +++ b/web/public/locales/pt-BR/components/auth.json @@ -10,6 +10,7 @@ "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 index 322e63522..03ee52b58 100644 --- a/web/public/locales/pt-BR/components/camera.json +++ b/web/public/locales/pt-BR/components/camera.json @@ -66,7 +66,8 @@ "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" } }, "debug": { diff --git a/web/public/locales/pt-BR/components/dialog.json b/web/public/locales/pt-BR/components/dialog.json index f180fe513..6f15f9855 100644 --- a/web/public/locales/pt-BR/components/dialog.json +++ b/web/public/locales/pt-BR/components/dialog.json @@ -108,7 +108,15 @@ "button": { "markAsReviewed": "Marcar como revisado", "export": "Exportar", - "deleteNow": "Deletar Agora" + "deleteNow": "Deletar Agora", + "markAsUnreviewed": "Marcar como não revisado" } + }, + "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" } } diff --git a/web/public/locales/pt-BR/components/filter.json b/web/public/locales/pt-BR/components/filter.json index d503d3f13..ee84e75d6 100644 --- a/web/public/locales/pt-BR/components/filter.json +++ b/web/public/locales/pt-BR/components/filter.json @@ -23,7 +23,7 @@ "short": "Datas" } }, - "more": "Mais filtros", + "more": "Mais Filtros", "reset": { "label": "Resetar filtros para valores padrão" }, @@ -71,7 +71,7 @@ "title": "Configurações", "defaultView": { "title": "Visualização Padrão", - "desc": "Quando nenhum filtro é selecionado, exibir um sumário dos objetos mais recentes rastreados por categoria, ou exiba uma grade sem filtro.", + "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" }, @@ -106,7 +106,7 @@ }, "trackedObjectDelete": { "title": "Confirmar Exclusão", - "desc": "Deletar esses {{objectLength}} objetos rastreados remove as capturas de imagem, qualquer 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.", + "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}}" @@ -121,6 +121,16 @@ "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." + "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" } } 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 000000000..c90529873 --- /dev/null +++ b/web/public/locales/pt-BR/views/classificationModel.json @@ -0,0 +1,55 @@ +{ + "documentTitle": "Modelos de Classificação", + "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": "Classe Apagada", + "deletedImage": "Imagens Apagadas", + "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" + }, + "error": { + "deleteImageFailed": "Falha ao deletar:{{errorMessage}}", + "deleteCategoryFailed": "Falha ao deletar classe:{{errorMessage}}", + "categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}", + "deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}", + "trainingFailed": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "trainingFailedToStart": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "updateModelFailed": "Falha ao atualizar modelo: {{errorMessage}}", + "renameCategoryFailed": "Falha ao renomear 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 apagar a classe" + }, + "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." + }, + "details": { + "scoreInfo": "A pontuação representa a média de confiança da classificação de todas as detecções deste objeto." + }, + "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" + } +} diff --git a/web/public/locales/pt-BR/views/configEditor.json b/web/public/locales/pt-BR/views/configEditor.json index 1bd110a6f..46c4808cb 100644 --- a/web/public/locales/pt-BR/views/configEditor.json +++ b/web/public/locales/pt-BR/views/configEditor.json @@ -12,5 +12,7 @@ "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 index 1cd63daf0..37785ab13 100644 --- a/web/public/locales/pt-BR/views/events.json +++ b/web/public/locales/pt-BR/views/events.json @@ -26,13 +26,33 @@ }, "markTheseItemsAsReviewed": "Marque estes itens como revisados", "newReviewItems": { - "button": "Novos Itens para Revisão", + "button": "Novos Itens para Revisar", "label": "Ver novos itens para revisão" }, "selected_one": "{{count}} selecionado(s)", - "documentTitle": "Revisão - Frigate", + "documentTitle": "Revisar - Frigate", "markAsReviewed": "Marcar como Revisado", "selected_other": "{{count}} selecionado(s)", "camera": "Câmera", - "detected": "detectado" + "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" + } + }, + "objectTrack": { + "trackedPoint": "Ponto rastreado", + "clickToSeek": "Clique para ir para esse horário" + }, + "zoomIn": "Ampliar", + "zoomOut": "Diminuir o zoom" } diff --git a/web/public/locales/pt-BR/views/explore.json b/web/public/locales/pt-BR/views/explore.json index a43ee2b17..bb3e6fdab 100644 --- a/web/public/locales/pt-BR/views/explore.json +++ b/web/public/locales/pt-BR/views/explore.json @@ -3,15 +3,15 @@ "generativeAI": "IA Generativa", "exploreMore": "Explorar mais objetos {{label}}", "exploreIsUnavailable": { - "title": "Explorar não está disponível", + "title": "A seção Explorar está indisponível", "embeddingsReindexing": { - "context": "Explorar pode ser usado depois da incorporação do objeto rastreado terminar a reindexação.", - "startingUp": "Começando…", - "estimatedTime": "Time estimado faltando:", - "finishingShortly": "Terminando em breve", + "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 incorporadas: ", - "descriptionsEmbedded": "Descrições incorporadas: ", + "thumbnailsEmbedded": "Miniaturas embedded: ", + "descriptionsEmbedded": "Descrições embedded: ", "trackedObjectsProcessed": "Objetos rastreados processados: " } }, @@ -24,7 +24,7 @@ "visionModelFeatureExtractor": "Extrator de características do modelo de visão" }, "tips": { - "context": "Você pode querer reindexar as incorporações de seus objetos rastreados uma vez que os modelos forem baixados.", + "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." @@ -43,26 +43,28 @@ "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 as seguintes categorias: {{objects}}" + "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-categoria atualizada com sucesso.", - "updatedLPR": "Placa de identificação atualizada com sucesso." + "updatedSublabel": "Sub-rótulo atualizado com sucesso.", + "updatedLPR": "Placa de identificação atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio requisitada com sucesso." }, "error": { "regenerate": "Falha ao ligar para {{provider}} para uma descrição nova: {{errorMessage}}", - "updatedSublabelFailed": "Falha ao atualizar sub-categoria: {{errorMessage}}", - "updatedLPRFailed": "Falha ao atualizar placa de identificação: {{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": "Categoria", + "label": "Rótulo", "editSubLabel": { - "title": "Editar sub-categoria", - "desc": "Nomeie uma nova sub categoria para esse(a) {{label}}", - "descNoLabel": "Nomeie uma nova sub-categoria para esse objeto rastreado" + "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", @@ -99,6 +101,9 @@ "tips": { "descriptionSaved": "Descrição salva com sucesso", "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Pontuação" } }, "trackedObjectDetails": "Detalhes do Objeto Rastreado", @@ -106,7 +111,8 @@ "details": "detalhes", "snapshot": "captura de imagem", "video": "vídeo", - "object_lifecycle": "ciclo de vida do obejto" + "object_lifecycle": "ciclo de vida do objeto", + "thumbnail": "thumbnail" }, "objectLifecycle": { "title": "Ciclo de Vida do Objeto", @@ -184,12 +190,20 @@ }, "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, qualquer embedding salvo, e quaisquer entradas de ciclo de vida de objeto associadas. Gravações desse objeto rastreado na visualização de Histórico NÃO serão deletadas.

    Tem certeza que quer prosseguir?" + "desc": "Deletar esse objeto rastreado remove a captura de imagem, quaisquer embeddings salvos, e quaisquer entradas de ciclo de vida de objeto associadas. Gravações desse objeto rastreado na visualização de Histórico NÃO serão deletadas.

    Tem certeza que quer prosseguir?" } }, "noTrackedObjects": "Nenhum Objeto Rastreado Encontrado", @@ -205,5 +219,11 @@ "error": "Falha ao detectar objeto rastreado {{errorMessage}}" } } + }, + "aiAnalysis": { + "title": "Análise de IA" + }, + "concerns": { + "label": "Preocupações" } } diff --git a/web/public/locales/pt-BR/views/exports.json b/web/public/locales/pt-BR/views/exports.json index 892f719d2..12a6dce45 100644 --- a/web/public/locales/pt-BR/views/exports.json +++ b/web/public/locales/pt-BR/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Compartilhar exportação", + "downloadVideo": "Baixar vídeo", + "editName": "Editar nome", + "deleteExport": "Apagar exportação" } } diff --git a/web/public/locales/pt-BR/views/faceLibrary.json b/web/public/locales/pt-BR/views/faceLibrary.json index d08b38110..ee3ccde38 100644 --- a/web/public/locales/pt-BR/views/faceLibrary.json +++ b/web/public/locales/pt-BR/views/faceLibrary.json @@ -15,7 +15,7 @@ }, "maxSize": "Tamanho máximo: {{size}}MB", "dropActive": "Solte a imagem aqui…", - "dropInstructions": "Arraste e solte uma imagem aqui, ou clique para selecionar" + "dropInstructions": "Arraste e solte ou cole uma imagem aqui ou clique para selecionar" }, "deleteFaceLibrary": { "title": "Apagar Nome", @@ -33,19 +33,19 @@ "new": "Criar Novo Rosto", "title": "Criar Coleção", "desc": "Criar uma nova coleção", - "nextSteps": "Para construir uma base forte:
  • Use a aba Teinar 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.
  • " + "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 {{count}} rostos? Essa ação não pode ser desfeita.", - "desc_other": "" + "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": "Sem rostos disponíveis", + "nofaces": "Nenhum rosto disponível", "pixels": "{{area}}px", "readTheDocs": "Leia a documentação", "steps": { @@ -58,7 +58,7 @@ }, "description": { "placeholder": "Informe um nome para esta coleção", - "addFace": "Passo a Passo para adicionar uma nova coleção a Biblioteca Facial.", + "addFace": "Adicione uma nova coleção à Biblioteca Facial subindo a sua primeira imagem.", "invalidName": "Nome inválido. Nomes podem incluir apenas letras, números, espaços, apóstrofos, sublinhados e hífenes." }, "documentTitle": "Biblioteca de rostos - Frigate", @@ -68,8 +68,8 @@ }, "collections": "Coleções", "train": { - "title": "Treinar", - "aria": "Selecionar treinar", + "title": "Reconhecimentos Recentes", + "aria": "Selecionar reconhecimentos recentes", "empty": "Não há tentativas recentes de reconhecimento facial" }, "selectFace": "Selecionar Rosto", diff --git a/web/public/locales/pt-BR/views/live.json b/web/public/locales/pt-BR/views/live.json index 97ca4675c..8fb79a81a 100644 --- a/web/public/locales/pt-BR/views/live.json +++ b/web/public/locales/pt-BR/views/live.json @@ -43,6 +43,14 @@ "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": { @@ -63,7 +71,7 @@ }, "snapshots": { "enable": "Permitir Capturas de Imagem", - "disable": "Desativar Campturas de Imagem" + "disable": "Desativar Capturas de Imagem" }, "audioDetect": { "enable": "Ativar Detecção de Áudio", @@ -78,8 +86,8 @@ "disable": "Ocultar Estatísticas de Transmissão" }, "manualRecording": { - "title": "Gravação Sob Demanda", - "tips": "Inicie um evento manual baseado nas configurações de retenção de gravação dessa câmera.", + "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." @@ -126,6 +134,9 @@ "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": { @@ -135,7 +146,8 @@ "recording": "Gravação", "snapshots": "Capturas de Imagem", "audioDetection": "Detecção de Áudio", - "autotracking": "Auto Rastreamento" + "autotracking": "Auto Rastreamento", + "transcription": "Transcrição de Áudio" }, "history": { "label": "Exibir gravação histórica" @@ -154,5 +166,20 @@ "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" + }, + "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/settings.json b/web/public/locales/pt-BR/views/settings.json index c5e0af438..7bbb597d6 100644 --- a/web/public/locales/pt-BR/views/settings.json +++ b/web/public/locales/pt-BR/views/settings.json @@ -7,9 +7,11 @@ "masksAndZones": "Editor de Máscara e Zona - Frigate", "motionTuner": "Ajuste de Movimento - Frigate", "object": "Debug - Frigate", - "general": "Configurações Gerais - Frigate", + "general": "Configurações de Interface de Usuário - Frigate", "frigatePlus": "Frigate+ Configurações- Frigate", - "notifications": "Configurações de notificação - Frigate" + "notifications": "Configurações de notificação - Frigate", + "cameraManagement": "Gerenciar Câmeras - Frigate", + "cameraReview": "Configurações de Revisão de Câmera - Frigate" }, "menu": { "ui": "UI", @@ -20,7 +22,11 @@ "frigateplus": "Frigate+", "motionTuner": "Ajuste de Movimento", "debug": "Depurar", - "enrichments": "Melhorias" + "enrichments": "Enriquecimentos", + "triggers": "Gatilhos", + "roles": "Papéis", + "cameraManagement": "Gerenciamento", + "cameraReview": "Revisar" }, "dialog": { "unsavedChanges": { @@ -37,12 +43,15 @@ "liveDashboard": { "title": "Painel em Tempo Real", "automaticLiveView": { - "label": "Visão em Tempo Real Automática", - "desc": "Automaticamente alterar para a visã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." + "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 sejam reproduzidos como vídeos em loop. Desative essa opção para mostrar apenas a imagens estáticas de alertas recentes nesse dispositivo / navegador." + "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" } }, "storedLayouts": { @@ -58,8 +67,8 @@ "recordingsViewer": { "title": "Visualizador de Gravações", "defaultPlaybackRate": { - "label": "Taxa Padrão de Reprodução", - "desc": "Taxa Padrão de Reprodução para Gravações." + "label": "Velocidade Padrão de Reprodução", + "desc": "Velocidade padrão de reprodução para gravações." } }, "calendar": { @@ -87,7 +96,7 @@ "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 commum será adicionado como uma subcategoria. Essa informação é incluida na UI, filtros e notificações." + "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", @@ -95,7 +104,7 @@ "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 100% da CPU e levar um tempo considerável dependendo do número de objetos rastreados que você possui.", + "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", @@ -108,7 +117,7 @@ "desc": "O tamanho do modelo usado para embeddings de pesquisa semântica.", "small": { "title": "pequeno", - "desc": "Usandopequeno 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." + "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", @@ -118,24 +127,24 @@ }, "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 uma sub-categoria. Essa informação é inclusa na UI, filtros e notificações.", + "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 pequeno emprega o modelo de embedding de rosto FaceNet, que roda de maneira eficiente na maioria das CPUs." + "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": "Usando o grande emprega um modelo de embedding de rosto ArcFace e irá automáticamente roda pela GPU se aplicável." + "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 uma sub-categoria 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.", + "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)", @@ -148,22 +157,22 @@ "title": "Configurações de Câmera", "streams": { "title": "Transmissões", - "desc": "Temporáriamente desativar a câmera até o Frigate reiniciar. Desatiar 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." + "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 habilitar/desabilitar alertas e detecções para essa câmera até o Frigate reiniciar. Quando desabilitado, nenhum novo item de revisão será gerado. ", + "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": "Revisar Classificação", - "desc": "O Frigate categoriza itens de revisão como Alertas e Detecções. Por padrão, todas as pessoa e carros são considerados alertas. Você pode refinar a categorização dos seus itens revisados configurando as zonas requeridas para eles.", + "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 {{alertsLabels}} objetos em {{cameraName}} serão exibidos como Alertas.", + "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": { @@ -176,6 +185,44 @@ "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": { @@ -359,16 +406,16 @@ "documentation": "Leia o Guia de Ajuste de Movimento" }, "Threshold": { - "title": "Limite", + "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 do contorno da área é usado para decidir quais grupos de mudança de pixel se qualificam como movimento. Padrão: 10" + "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: ON" + "desc": "Melhorar contraste para cenas escuras. Padrão: Ativado" }, "toast": { "success": "As configurações de movimento foram salvas." @@ -420,14 +467,27 @@ "noObjects": "Nenhum Objeto", "timestamp": { "title": "Timestamp", - "desc": "Sobreponha um timestamp na imagem" - } + "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": "Gerencias as contas de usuário dessa instância do Frigate." + "desc": "Gerenciar as contas de usuário dessa instância do Frigate." }, "addUser": "Adicionar Usuário", "updatePassword": "Atualizar Senha", @@ -506,7 +566,8 @@ "admin": "Administrador", "adminDesc": "Acesso total a todos os recursos.", "viewer": "Espectador", - "viewerDesc": "Limitado aos Painéis ao Vivo, Revisar, Explorar, e Exportar somente." + "viewerDesc": "Limitado aos Painéis ao Vivo, Revisar, Explorar, e Exportar somente.", + "customDesc": "Papel customizado com acesso a câmeras específicas." } } }, @@ -580,7 +641,7 @@ "apiKey": { "title": "Chave de API do Frigate+", "validated": "A chave de API do Frigate+ detectada e validada", - "notValidated": "A chave de API do Frigate+ não detectada ou não 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+" }, @@ -603,7 +664,7 @@ }, "snapshotConfig": { "title": "Configuração de Captura de Imagem", - "desc": "Enviar ao Frigate+ requer tanto a captura de imagem quanto a captura de imagem clean_copy estarem habilitadas na sua configuração.", + "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": { @@ -618,5 +679,228 @@ "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": "Gerenciamento de 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": "Digite o nome do gatilho", + "error": { + "minLength": "O nome precisa ter no mínimo 2 caracteres.", + "invalidCharacters": "O nome pode contar apenas letras, números, sublinhados, e hífens.", + "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 uma imagem", + "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(os) atribuídos a esse papel foram atualizados para 'visualizador', que possui acesso a todas as câmeras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" + }, + "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": "Adicione os detalhes da sua câmera e teste a conexão.", + "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" + }, + "testing": { + "probingMetadata": "Inferindo o metadata da câmera...", + "fetchingSnapshot": "Buscando a captura de imagem da câmera..." + } + } } } diff --git a/web/public/locales/pt-BR/views/system.json b/web/public/locales/pt-BR/views/system.json index 74a2c4564..4875d8015 100644 --- a/web/public/locales/pt-BR/views/system.json +++ b/web/public/locales/pt-BR/views/system.json @@ -42,7 +42,8 @@ "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" + "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", @@ -102,6 +103,10 @@ "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": { @@ -157,8 +162,9 @@ "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 vetores de característica de imagens ({{processed}}% completado)", - "detectIsSlow": "{{detect}} está lento ({{speed}} ms)" + "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", @@ -167,13 +173,13 @@ "face_recognition": "Reconhecimento Facial", "plate_recognition": "Reconhecimento de Placa", "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", - "text_embedding_speed": "Velocidade de Geração de Vetores de Texto", + "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": "Vetores de Características de Imagens", - "text_embedding": "Vetor de Característica de Texto", - "image_embedding_speed": "Velocidade de Geração de Vetores de Imagem", - "face_embedding_speed": "Velocidade de Geração de Vetores de Rostos", + "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 index 36b414716..3bf1ba60b 100644 --- a/web/public/locales/pt/audio.json +++ b/web/public/locales/pt/audio.json @@ -1,8 +1,8 @@ { - "babbling": "Balbuciar", + "babbling": "Falador", "speech": "Discurso", "whoop": "Grito de Alegria", - "bellow": "Abaixo", + "bellow": "Debaixo", "yell": "Gritar", "whispering": "Sussurrar", "child_singing": "Criança a Cantar", @@ -14,7 +14,7 @@ "meow": "Miau", "run": "Correr", "sheep": "Ovelha", - "motorcycle": "Motociclo", + "motorcycle": "Mota", "car": "Carro", "cat": "Gato", "horse": "Cavalo", @@ -33,15 +33,15 @@ "whistling": "Assobiar", "wheeze": "Chiadeira", "gasp": "Ofegar", - "cough": "Tosse", - "sneeze": "Espirro", + "cough": "Tossir", + "sneeze": "Espirrar", "footsteps": "Passos", "chewing": "Mastigar", "biting": "Morder", "gargling": "Gargarejar", "stomach_rumble": "Ronco de Estômago", "burping": "Arroto", - "hiccup": "Solavanco", + "hiccup": "Soluço", "fart": "Pum", "hands": "Mãos", "finger_snapping": "Estalar os Dedos", @@ -109,7 +109,7 @@ "helicopter": "Helicóptero", "engine": "Motor", "coin": "Moeda", - "scissors": "Tesoura", + "scissors": "Tesouras", "electric_shaver": "Barbeador Elétrico", "computer_keyboard": "Teclado de Computador", "alarm": "Alarme", @@ -145,12 +145,12 @@ "owl": "Coruja", "mouse": "Rato", "vehicle": "Veículo", - "hair_dryer": "Secador de cabelo", - "toothbrush": "Escova de dentes", - "sink": "Pia", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "sink": "Banca", "blender": "Liquidificador", "pant": "Ofegar", - "snort": "Espirrar pelo Nariz", + "snort": "Resfolegar", "throat_clearing": "Limpar a Garganta", "sniff": "Cheirar", "shuffle": "Embaralhar", diff --git a/web/public/locales/pt/common.json b/web/public/locales/pt/common.json index ad63195c1..557d6b48d 100644 --- a/web/public/locales/pt/common.json +++ b/web/public/locales/pt/common.json @@ -2,13 +2,13 @@ "time": { "last30": "Últimos 30 dias", "12hours": "12 horas", - "justNow": "Agora", + "justNow": "Agora mesmo", "yesterday": "Ontem", "today": "Hoje", "last7": "Últimos 7 dias", "last14": "Últimos 14 dias", - "thisWeek": "Essa semana", - "lastWeek": "Semana passada", + "thisWeek": "Esta Semana", + "lastWeek": "Semana Passada", "5minutes": "5 minutos", "10minutes": "10 minutos", "30minutes": "30 minutos", @@ -18,14 +18,14 @@ "year_one": "{{time}} ano", "year_many": "{{time}} de anos", "year_other": "{{time}} anos", - "month_one": "{{time}} mes", + "month_one": "{{time}} mês", "month_many": "{{time}} meses", - "month_other": "", + "month_other": "{{time}} meses", "day_one": "{{time}} dia", "day_many": "{{time}} dias", "day_other": "{{time}} dias", - "thisMonth": "Esse mês", - "lastMonth": "Mês passado", + "thisMonth": "Este Mês", + "lastMonth": "Mês Passado", "1hour": "1 hora", "hour_one": "{{time}} hora", "hour_many": "{{time}} horas", @@ -39,7 +39,7 @@ "untilForTime": "Até {{time}}", "untilForRestart": "Até que o Frigate reinicie.", "untilRestart": "Até reiniciar", - "ago": "{{timeAgo}} atrás", + "ago": "há {{timeAgo}}", "d": "{{time}}d", "h": "{{time}}h", "m": "{{time}}m", @@ -87,68 +87,85 @@ "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" }, "unit": { "speed": { - "kph": "kph", + "kph": "km/h", "mph": "mph" }, "length": { - "feet": "pé", + "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": "Habilitado", - "enable": "Habilitar", + "enabled": "Ativado", + "enable": "Ativar", "done": "Feito", "reset": "Reiniciar", - "disabled": "Desabilitado", - "saving": "Salvando…", + "disabled": "Desativado", + "saving": "A guardar…", "apply": "Aplicar", - "disable": "Desabilitar", + "disable": "Desativar", "save": "Salvar", - "copy": "Cópia", + "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", + "twoWayTalk": "Conversa Bidirecional", + "cameraAudio": "Áudio da Câmera", "edit": "Editar", "off": "DESLIGADO", "copyCoordinates": "Copiar coordenadas", "on": "LIGADO", - "delete": "Excluir", - "download": "Download", - "info": "Informações", + "delete": "Eliminar", + "download": "Transferir", + "info": "Informação", "no": "Não", "suspended": "Suspenso", "yes": "Sim", - "unselect": "Desmarcar", + "unselect": "Desselecionar", "unsuspended": "Dessuspender", - "deleteNow": "Excluir agora", + "deleteNow": "Eliminar Agora", "export": "Exportar", - "next": "Próximo", + "next": "Seguinte", "play": "Tocar", - "pictureInPicture": "Sobrepor Imagem" + "pictureInPicture": "Imagem sobre Imagem", + "continue": "Continuar" }, "label": { - "back": "Voltar" + "back": "Voltar", + "hide": "Ocultar {{item}}", + "show": "Exibir {{item}}", + "ID": "ID", + "none": "Nenhum", + "all": "Todos" }, "menu": { "user": { - "logout": "Sair", + "logout": "Terminar sessão", "account": "Conta", "current": "Utilizador atual: {{user}}", - "setPassword": "Definir senha", + "setPassword": "Definir Palavra-passe", "title": "Utilizador", - "anonymous": "anônimo" + "anonymous": "anónimo" }, - "faceLibrary": "Biblioteca de rostos", + "faceLibrary": "Biblioteca de Rostos", "withSystem": "Sistema", "theme": { "label": "Tema", @@ -156,58 +173,66 @@ "green": "Verde", "red": "Vermelho", "contrast": "Alto contraste", - "default": "Padrão", + "default": "Predefinição", "highcontrast": "Alto Contraste", "nord": "Nord" }, "system": "Sistema", "systemMetrics": "Métricas do sistema", "configuration": "Configuração", - "systemLogs": "Logs do sistema", - "settings": "Configurações", - "configurationEditor": "Editor de configuração", + "systemLogs": "Registos do sistema", + "settings": "Definições", + "configurationEditor": "Editor de Configuração", "languages": "Idiomas", "language": { - "en": "Inglês (English)", - "zhCN": "Chinês simplificado", + "en": "Inglês (EUA)", + "zhCN": "简体中文 (Chinês Simplificado)", "withSystem": { - "label": "Use as configurações do sistema para idioma" + "label": "Utilizar as definições do sistema para o idioma" }, - "fr": "Français (Francês)", - "es": "Español (Espanhol)", - "ru": "Русский (Russo)", - "de": "Deutsch (Alemão)", - "ja": "日本語 (Japonês)", - "yue": "Cantonês (粵語)", - "ar": "العربية (Arabic)", - "uk": "Ucraniano (Українська)", - "el": "Grego (Ελληνικά)", - "hi": "हिन्दी (Hindi)", - "pt": "Português (Portuguese)", - "tr": "Türkçe (Turkish)", - "it": "Italiano (Italian)", - "nb": "Norueguês Bokmål (Norsk Bokmål)", - "ko": "Coreano (한국어)", - "vi": "Vietnamita (Tiếng Việt)", - "nl": "Nederlands (Dutch)", - "sv": "Svenska (Swedish)", - "cs": "Tcheco (Čeština)", - "fa": "Persa (فارسی)", - "pl": "Polonês (Polski)", - "he": "Hebraico (עברית)", - "fi": "Finlandês (Suomi)", - "da": "Dinamarquês (Dansk)", - "ro": "Romeno (Română)", - "hu": "Húngaro (Magyar)", - "sk": "Eslovaco (Slovenčina)", + "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à (Catalão)" + "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" }, "appearance": "Aparência", "darkMode": { - "label": "Modo escuro", + "label": "Modo Escuro", "withSystem": { - "label": "Use as configurações do sistema para o modo claro ou escuro" + "label": "Utilizar as definições do sistema para o modo claro ou escuro" }, "light": "Claro", "dark": "Escuro" @@ -220,7 +245,7 @@ "restart": "Reiniciar Frigate", "live": { "title": "Ao vivo", - "allCameras": "Todas as câmaras", + "allCameras": "Todas as Câmaras", "cameras": { "title": "Câmaras", "count_one": "{{count}} Câmera", @@ -230,8 +255,9 @@ }, "export": "Exportar", "explore": "Explorar", - "review": "Análise", - "uiPlayground": "Área de Testes da Interface" + "review": "Rever", + "uiPlayground": "Área de Testes da IU", + "classification": "Classificação" }, "pagination": { "previous": { @@ -240,36 +266,49 @@ }, "label": "paginação", "next": { - "title": "Próximo", - "label": "Ir para a próxima página" + "title": "Seguinte", + "label": "Ir para a página seguinte" }, "more": "Mais páginas" }, "role": { "admin": "Administrador", "viewer": "Visualizador", - "title": "Regra", - "desc": "Administradores têm acesso total a todos os recursos da interface do Frigate. Visualizadores estão limitados a visualizar câmeras, revisar itens e assistir o histórico de gravaçoes na interface." + "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 copiada para a área de transferência.", + "copyUrlToClipboard": "URL copiado para a área de transferência.", "save": { - "title": "Salvar", + "title": "Guardar", "error": { - "noMessage": "Falha ao salvar as alterações de configuração", - "title": "Falha ao salvar as alterações de configuração: {{errorMessage}}" + "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}}" } } }, "accessDenied": { - "documentTitle": "Acesso negado - Frigate", - "title": "Acesso negado", - "desc": "Você não tem permissão para visualizar esta página." + "documentTitle": "Frigate - Acesso Negado", + "title": "Acesso Negado", + "desc": "Não tem permissão para ver esta página." }, "notFound": { - "documentTitle": "Não encontrado - Frigate", + "documentTitle": "Frigate - Não Encontrado", "desc": "Página não encontrada", "title": "404" }, - "selectItem": "Selecionar {{item}}" + "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" + } } diff --git a/web/public/locales/pt/components/auth.json b/web/public/locales/pt/components/auth.json index 5dcccd7d6..3fa777ba8 100644 --- a/web/public/locales/pt/components/auth.json +++ b/web/public/locales/pt/components/auth.json @@ -1,15 +1,16 @@ { "form": { "user": "Nome do utilizador", - "login": "Login", + "login": "Iniciar sessão", "errors": { "usernameRequired": "O nome do utilizador é obrigatório", - "passwordRequired": "Senha é necessária", + "passwordRequired": "A palavra-passe é obrigatória", "rateLimit": "Limite de taxa excedido. Tente novamente mais tarde.", - "loginFailed": "Falha no login", - "unknownError": "Erro desconhecido. Verifique os logs.", - "webUnknownError": "Erro desconhecido. Verifique os logs da consola." + "loginFailed": "Autenticação falhou", + "unknownError": "Erro desconhecido. Verifique os registos.", + "webUnknownError": "Erro desconhecido. Verifique os registos da consola." }, - "password": "Senha" + "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 index fa4a5fdc1..3f7052c81 100644 --- a/web/public/locales/pt/components/camera.json +++ b/web/public/locales/pt/components/camera.json @@ -1,27 +1,27 @@ { "group": { - "label": "Grupos de câmaras", - "add": "Adicionar grupo de câmaras", - "edit": "Editar grupo de câmaras", + "label": "Grupos de Câmaras", + "add": "Adicionar Gupo de Câmaras", + "edit": "Editar Grupo de Câmaras", "delete": { - "label": "Excluir grupo de câmaras", + "label": "Eliminar Grupo de Câmaras", "confirm": { - "title": "Confirmar exclusão", - "desc": "Tem certeza de que deseja excluir o grupo de câmaras {{name}}?" + "title": "Confirmar Eliminar", + "desc": "Tem a certeza que deseja eliminar o grupo de câmaras {{name}}?" } }, "name": { "label": "Nome", - "placeholder": "Digita um 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 caracteres.", - "invalid": "Nome de grupo de câmaras inválido." + "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 câmaras para este grupo.", + "desc": "Selecione as câmaras para este grupo.", "label": "Câmaras" }, "icon": "Ícone", @@ -37,17 +37,17 @@ } }, "streamMethod": { - "label": "Método de transmissão", + "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 nenhuma atividade detectável estiver ocorrendo para conservar largura de banda e recursos. Quando a atividade é detectada, a imagem muda perfeitamente para uma transmissão ao vivo." + "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", + "label": "Transmissão Contínua", "desc": { - "warning": "A transmissão contínua pode causar alto uso de largura de banda e problemas de desempenho. Use com precaução.", - "title": "A imagem da câmara sempre será uma transmissão ao vivo quando visível no painel, mesmo que nenhuma atividade esteja sendo detectada." + "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": { @@ -59,24 +59,25 @@ }, "compatibilityMode": { "label": "Modo de compatibilidade", - "desc": "Habilite esta opção somente se a transmissão ao vivo da sua câmara estiver exibindo artefatos de cor e tiver uma linha diagonal no lado direito da imagem." + "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": "Configuraçõ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. Essas configurações são específicas do dispositivo/navegador.", - "title": "{{cameraName}} configurações de transmissão", + "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" } }, "debug": { "options": { - "label": "Configurações", + "label": "Definições", "title": "Opções", - "hideOptions": "Ocultar opções", - "showOptions": "Mostrar opções" + "hideOptions": "Ocultar Opções", + "showOptions": "Mostrar Opções" }, - "boundingBox": "Caixa delimitadora", + "boundingBox": "Caixa Delimitadora", "timestamp": "Carimbo de hora", "zones": "Zonas", "mask": "Máscara", diff --git a/web/public/locales/pt/components/dialog.json b/web/public/locales/pt/components/dialog.json index 766711539..b1aeb06c1 100644 --- a/web/public/locales/pt/components/dialog.json +++ b/web/public/locales/pt/components/dialog.json @@ -2,17 +2,17 @@ "restart": { "button": "Reiniciar", "restarting": { - "title": "Frigate está reiniciando", + "title": "Frigate está a reiniciar", "content": "Esta página será recarregada em {{countdown}} segundos.", - "button": "Forçar atualização agora" + "button": "Forçar Recarregar Agora" }, - "title": "Tem certeza de que deseja reiniciar o Frigate?" + "title": "Tem a certeza que deseja reiniciar o Frigate?" }, "explore": { "plus": { "submitToPlus": { - "label": "Enviar para Frigate+", - "desc": "Objetos em locais que você quer evitar não são falsos positivos. Enviá-los como falsos positivos confundirá o modelo." + "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": { @@ -22,7 +22,7 @@ "true_other": "Estão são {{label}}" }, "state": { - "submitted": "Enviado" + "submitted": "Submetido" }, "false": { "label": "Não confirmar esta etiqueta para Frigate Plus", @@ -31,15 +31,15 @@ "false_other": "Estes não são {{label}}" }, "question": { - "label": "Confirme este rótulo para Frigate Plus", + "label": "Confirme esta etiqueta para Frigate Plus", "ask_a": "Este objeto é um {{label}}?", "ask_an": "Este objeto é um {{label}}?", - "ask_full": "Este objeto é um(a) {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Este objeto é um {{untranslatedLabel}} ({{translatedLabel}})?" } } }, "video": { - "viewInHistory": "Ver no histórico" + "viewInHistory": "Ver no Histórico" } }, "export": { @@ -60,67 +60,74 @@ }, "export": "Exportar", "toast": { - "success": "Exportação iniciada com sucesso. Veja o arquivo na pasta /exports.", + "success": "Exportação iniciada com sucesso. Veja o ficheiro na pasta de exportações.", "error": { - "failed": "Falha ao iniciar a exportação: {{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": "Salvar exportação", - "previewExport": "Visualizar exportação" + "saveExport": "Guardar Exportação", + "previewExport": "Pré-visualizar Exportação" }, - "select": "Selecione", + "select": "Selecionar", "name": { - "placeholder": "Nome da exportação" + "placeholder": "Nome da Exportação" } }, "streaming": { "showStats": { "label": "Mostrar estatísticas de transmissão", - "desc": "Habilite esta opção para mostrar estatísticas de transmissão como uma sobreposição no feed da câmara." + "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 o go2rtc para obter opções adicionais de visualização ao vivo e áudio para esta câmara.", + "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á habilitada para esta câmara." + "disabled": "A retransmissão não está ativada para esta câmara." }, "label": "Transmissão", - "debugView": "Exibição de depuração" + "debugView": "Ver Depuração" }, "search": { "saveSearch": { - "label": "Salvar pesquisa", - "overwrite": "{{searchName}} já existe. Salvar substituirá o valor existente.", - "success": "A pesquisa ({{searchName}}) foi salva.", + "label": "Guardar Procura", + "overwrite": "{{searchName}} já existe. Ao guardar irá substituir o valor existente.", + "success": "A procura ({{searchName}}) foi guardada.", "button": { "save": { - "label": "Salvar esta pesquisa" + "label": "Guardar esta procura" } }, - "placeholder": "Digite um nome para sua pesquisa", - "desc": "Forneça um nome para esta pesquisa salva." + "placeholder": "Insira um nome para a sua procura", + "desc": "Forneça um nome para esta procura guardada." } }, "recording": { "confirmDelete": { - "title": "Confirmar exclusão", + "title": "Confirmar Eliminar", "desc": { - "selected": "Tem certeza de que deseja excluir todos os vídeos gravados associados a este item de analise?

    Segure a tecla Shift para ignorar esta caixa de diálogo no futuro." + "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 aos itens de analise selecionados foram excluídas com êxito.", - "error": "Falhou a apagar: {{error}}" + "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": "Excluir agora" + "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 index 53f56241f..3f7fce7b8 100644 --- a/web/public/locales/pt/components/filter.json +++ b/web/public/locales/pt/components/filter.json @@ -13,18 +13,18 @@ "zones": { "label": "Zonas", "all": { - "title": "Todas as zonas", + "title": "Todas as Zonas", "short": "Zonas" } }, "dates": { "all": { - "title": "Todas as datas", + "title": "Todas as Datas", "short": "Datas" }, - "selectPreset": "Escolhe uma predefinição…" + "selectPreset": "Selecionar um Pré-ajuste…" }, - "more": "Mais filtros", + "more": "Mais Filtros", "reset": { "label": "Redefinir filtros para valores padrão" }, @@ -35,28 +35,28 @@ "score": "Pontuação", "features": { "label": "Funcionalidades", - "hasSnapshot": "Tem um snapshot", + "hasSnapshot": "Tem uma captura", "hasVideoClip": "Tem um videoclipe", "submittedToFrigatePlus": { - "label": "Enviado para Frigate+", - "tips": "Primeiro, você deve filtrar os objetos rastreados que têm um snapshot.

    Objetos rastreados sem um snapshot não podem ser enviados ao Frigate+." + "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": "Organizar", + "label": "Ordenar", "dateAsc": "Data (Ascendente)", - "scoreAsc": "Pontuação do objeto (Crescente)", - "scoreDesc": "Pontuação do objeto (Decrescente)", - "speedDesc": "Velocidade estimada (Decrescente)", - "speedAsc": "Velocidade estimada (Crescente)", + "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", + "label": "Filtro de Câmaras", "all": { "short": "Câmaras", - "title": "Todas as câmaras" + "title": "Todas as Câmaras" } }, "review": { @@ -67,22 +67,22 @@ }, "explore": { "settings": { - "title": "Configurações", + "title": "Definições", "defaultView": { - "title": "Exibição padrão", - "summary": "Sumário", - "unfilteredGrid": "Grade não filtrada", - "desc": "Quando nenhum filtro for selecionado, exiba um resumo dos objetos rastreados mais recentemente por etiqueta ou exiba uma grade não filtrada." + "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 grade", - "desc": "Selecione o número de colunas na visualização em grade." + "title": "Colunas da Grelha", + "desc": "Selecione o número de colunas na visualização em grelha." }, "searchSource": { - "label": "Pesquisar fonte", - "desc": "Escolha se deseja pesquisar nas miniaturas ou descrições dos seus objetos rastreados.", + "label": "Procurar Fonte", + "desc": "Escolha se deseja procurar nas miniaturas ou descrições dos seus objetos rastreados.", "options": { - "thumbnailImage": "Imagem em miniatura", + "thumbnailImage": "Imagem em Miniatura", "description": "Descrição" } } @@ -94,14 +94,14 @@ } }, "logSettings": { - "label": "Nível de log do filtro", + "label": "Nível de registo do filtro", "loading": { - "title": "Carregando", - "desc": "Ao fazer scroll até ao fundo no painel de logs, novos registos são automaticamente apresentados à medida que são adicionados." + "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 logs por gravidade", - "disableLogStreaming": "Desativar transmissão de logs", - "allLogs": "Todos os logs" + "filterBySeverity": "Filtrar registos por gravidade", + "disableLogStreaming": "Desativar transmissão de registos", + "allLogs": "Todos os registos" }, "estimatedSpeed": "Velocidade estimada ({{unit}})", "timeRange": "Intervalo de tempo", @@ -109,19 +109,29 @@ "filterBy": "Filtrar por máscara de zona" }, "trackedObjectDelete": { - "title": "Confirmar exclusão", + "title": "Confirmar Eliminar", "toast": { - "success": "Objetos rastreados excluídos com sucesso.", - "error": "Falha ao excluir os objetos rastreados: {{errorMessage}}" + "success": "Objetos rastreados eliminados com sucesso.", + "error": "Não foi possível eliminar os objetos rastreados: {{errorMessage}}" }, - "desc": "Excluir estes {{objectLength}} objetos rastreados remove a captura de imagem, quaisquer embeddings salvos 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 excluídas.

    Tem certeza de que deseja continuar?

    Mantenha pressionada a tecla Shift para ignorar este diálogo no futuro." + "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": "Placas Reconhecidas", - "noLicensePlatesFound": "Nenhuma matrícula encontrada.", - "selectPlatesFromList": "Selecione uma ou mais placas da lista.", - "loadFailed": "Falha ao carregar as placas reconhecidas.", - "loading": "Carregando placas reconhecidas…", - "placeholder": "Digite para procurar placas…" + "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 index ddd38e84c..71b767a1d 100644 --- a/web/public/locales/pt/components/icons.json +++ b/web/public/locales/pt/components/icons.json @@ -2,7 +2,7 @@ "iconPicker": { "selectIcon": "Selecione um ícone", "search": { - "placeholder": "Pesquisar por um ícone…" + "placeholder": "Procurar por um ícone…" } } } diff --git a/web/public/locales/pt/components/input.json b/web/public/locales/pt/components/input.json index 3332f0820..1324ed188 100644 --- a/web/public/locales/pt/components/input.json +++ b/web/public/locales/pt/components/input.json @@ -1,9 +1,9 @@ { "button": { "downloadVideo": { - "label": "Descarregar vídeo", + "label": "Transferir Vídeo", "toast": { - "success": "O vídeo do seu item de análise começou a ser descarregado." + "success": "O vídeo do seu item de análise começou a ser transferido." } } } diff --git a/web/public/locales/pt/components/player.json b/web/public/locales/pt/components/player.json index 301e3f60d..741d37ef0 100644 --- a/web/public/locales/pt/components/player.json +++ b/web/public/locales/pt/components/player.json @@ -1,12 +1,12 @@ { - "noPreviewFound": "Nenhuma visualização encontrada", - "noPreviewFoundFor": "Nenhuma visualização encontrada para {{cameraName}}", + "noPreviewFound": "Nenhuma pré-visualização encontrada", + "noPreviewFoundFor": "Nenhuma pré-visualização encontrada para {{cameraName}}", "submitFrigatePlus": { - "title": "Enviar este quadro para o Frigate+?", - "submit": "Enviar" + "title": "Submeter esta imagem para Frigate+?", + "submit": "Submeter" }, "streamOffline": { - "title": "Transmissão offline", + "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", @@ -29,23 +29,23 @@ }, "totalFrames": "Total de quadros:", "droppedFrames": { - "title": "Quadros perdidos:", + "title": "Imagens perdidas:", "short": { - "title": "Perdido", - "value": "{{droppedFrames}} quadros" + "title": "Perdida", + "value": "{{droppedFrames}} imagens" } }, "decodedFrames": "Quadros decodificados:", - "droppedFrameRate": "Taxa de Quadros Perdidos:" + "droppedFrameRate": "Taxa de imagem perdida:" }, "noRecordingsFoundForThisTime": "Nenhuma gravação encontrada para este momento", - "livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para este tipo de transmissão ao vivo.", + "livePlayerRequiredIOSVersion": "É necessário o iOS 17.1 ou superior para este tipo de transmissão ao vivo.", "toast": { "success": { - "submittedFrigatePlus": "Quadro enviado com sucesso para o Frigate+" + "submittedFrigatePlus": "Imagem submetida com sucesso para Frigate+" }, "error": { - "submitFrigatePlusFailed": "Falha ao enviar o quadro para o Frigate+" + "submitFrigatePlusFailed": "Não foi possível submeter a imagem para Frigate+" } } } diff --git a/web/public/locales/pt/objects.json b/web/public/locales/pt/objects.json index ada61d184..88762a7f3 100644 --- a/web/public/locales/pt/objects.json +++ b/web/public/locales/pt/objects.json @@ -2,16 +2,16 @@ "giraffe": "Girafa", "cup": "Chávena", "person": "Pessoa", - "stop_sign": "Sinal de Stop", + "stop_sign": "Sinal de Parar", "sheep": "Ovelha", - "sandwich": "Sandes", + "sandwich": "Sande", "carrot": "Cenoura", - "dining_table": "Mesa de jantar", - "motorcycle": "Motociclo", + "dining_table": "Mesa de Jantar", + "motorcycle": "Mota", "bicycle": "Bicicleta", - "street_sign": "Sinal de rua", + "street_sign": "Sinal de Rua", "pizza": "Pizza", - "parking_meter": "Parquímetro", + "parking_meter": "Parcómetro", "skateboard": "Skate", "bottle": "Garrafa", "car": "Carro", @@ -23,19 +23,19 @@ "fire_hydrant": "Boca de Incêndio", "bird": "Pássaro", "cat": "Gato", - "bench": "Banco de jardim/rua", + "bench": "Banco de Jardim", "elephant": "Elefante", "hat": "Chapéu", "backpack": "Mochila", "shoe": "Sapato", - "handbag": "Bolsa de mão", + "handbag": "Carteira", "tie": "Gravata", - "suitcase": "Mala de viagem", + "suitcase": "Mala de Viagem", "frisbee": "Disco de Frisbee", "skis": "Esquis", - "kite": "Kite", - "baseball_bat": "Taco basebol", - "tennis_racket": "Raquete de Tenis", + "kite": "Papagaio de Papel", + "baseball_bat": "Taco de Basebol", + "tennis_racket": "Raquete de Ténis", "plate": "Prato", "wine_glass": "Copo de Vinho", "fork": "Garfo", @@ -43,17 +43,17 @@ "bowl": "Tijela", "banana": "Banana", "apple": "Maça", - "hot_dog": "Cachorro quente", + "hot_dog": "Cachorro Quente", "donut": "Donut", "cake": "Bolo", "chair": "Cadeira", - "potted_plant": "Planta em vaso", + "potted_plant": "Planta em Vaso", "mirror": "Espelho", - "desk": "Mesa", + "desk": "Escrivaninha", "toilet": "Casa de Banho", "door": "Porta", - "baseball_glove": "Luva de beisebol", - "surfboard": "Prancha de surf", + "baseball_glove": "Luva de Basebol", + "surfboard": "Prancha de Surf", "broccoli": "Brócolos", "snowboard": "Snowboard", "dog": "Cão", @@ -74,22 +74,22 @@ "bark": "Latido", "goat": "Cabra", "vehicle": "Veículo", - "scissors": "Tesoura", + "scissors": "Tesouras", "mouse": "Rato", - "teddy_bear": "Urso de peluche", - "hair_dryer": "Secador de cabelo", - "toothbrush": "Escova de dentes", + "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": "Controlo Remoto", + "remote": "Comando", "cell_phone": "Telemóvel", "microwave": "Microondas", "oven": "Forno", "toaster": "Torradeira", - "sink": "Pia", + "sink": "Banca", "refrigerator": "Frigorífico", "blender": "Liquidificador", "book": "Livro", @@ -98,7 +98,7 @@ "fox": "Raposa", "rabbit": "Coelho", "raccoon": "Guaxinim", - "robot_lawnmower": "Robô corta relva", + "robot_lawnmower": "Robô de Cortar Relva", "waste_bin": "Contentor do Lixo", "on_demand": "On Demand", "face": "Rosto", diff --git a/web/public/locales/pt/views/classificationModel.json b/web/public/locales/pt/views/classificationModel.json new file mode 100644 index 000000000..2ab7c1fa8 --- /dev/null +++ b/web/public/locales/pt/views/classificationModel.json @@ -0,0 +1,50 @@ +{ + "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": "Classe excluída", + "deletedImage": "Imagens excluídas", + "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." + } +} diff --git a/web/public/locales/pt/views/configEditor.json b/web/public/locales/pt/views/configEditor.json index 6d6c98166..fb1d3377a 100644 --- a/web/public/locales/pt/views/configEditor.json +++ b/web/public/locales/pt/views/configEditor.json @@ -1,16 +1,18 @@ { - "configEditor": "Editor de configuração", - "copyConfig": "Copiar configuração", - "saveAndRestart": "Salvar e reiniciar", - "saveOnly": "Salvar Apenas", + "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 salvar configuração" + "savingError": "Erro ao guardar a configuração" } }, - "documentTitle": "Editor de configuração - Frigate", - "confirm": "Sair sem salvar?" + "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 index 6478001c6..bb9b2e0ff 100644 --- a/web/public/locales/pt/views/events.json +++ b/web/public/locales/pt/views/events.json @@ -1,10 +1,10 @@ { - "detections": "Detecções", + "detections": "Deteções", "motion": { "label": "Movimento", - "only": "Somente movimento" + "only": "Apenas movimento" }, - "allCameras": "Todas as câmaras", + "allCameras": "Todas as Câmaras", "empty": { "motion": "Nenhum dado de movimento encontrado", "alert": "Não há alertas para análise", @@ -20,7 +20,7 @@ "alerts": "Alertas", "documentTitle": "Análise - Frigate", "recordings": { - "documentTitle": "Gravações - Frigate" + "documentTitle": "Frigate - Gravações" }, "calendarFilter": { "last24Hours": "Últimas 24 horas" @@ -32,7 +32,9 @@ "button": "Novos itens para analisar" }, "camera": "Câmara", - "detected": "detectado", + "detected": "detetado", "selected_one": "{{count}} selecionado", - "selected_other": "{{count}} selecionados" + "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 index a271d1df7..721508174 100644 --- a/web/public/locales/pt/views/explore.json +++ b/web/public/locales/pt/views/explore.json @@ -2,7 +2,7 @@ "generativeAI": "IA Generativa", "exploreIsUnavailable": { "embeddingsReindexing": { - "startingUp": "Iniciando…", + "startingUp": "A iniciar…", "estimatedTime": "Tempo restante estimado:", "finishingShortly": "Terminando em breve", "step": { @@ -17,14 +17,14 @@ "visionModel": "Modelo de visão", "textModel": "Modelo de texto", "textTokenizer": "Tokenizador de texto", - "visionModelFeatureExtractor": "Extrator de características de modelo de visão" + "visionModelFeatureExtractor": "Extrator de funcionalidade de modelo de visão" }, - "context": "O Frigate está descarregando os modelos de incorporação necessários para dar suporte a funcionalidade de pesquisa semântica. Isso pode levar vários minutos, dependendo da velocidade da sua conexão de rede.", + "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 você queira reindexar as incorporações dos seus objetos rastreados depois que os modelos forem descarregados.", + "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 logs do Frigate." + "error": "Ocorreu um erro. Verifique os registos do Frigate." }, "title": "Explorar não está disponível" }, @@ -43,12 +43,14 @@ "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." + "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}}" + "updatedLPRFailed": "Falha ao atualizar a matrícula: {{errorMessage}}", + "audioTranscription": "Falha ao solicitar transcrição de áudio: {{errorMessage}}" } }, "button": { @@ -97,27 +99,30 @@ "tips": { "descriptionSaved": "Descrição salva com sucesso", "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Classificação" } }, - "documentTitle": "Explorar - Frigate", + "documentTitle": "Frigate - Explorar", "trackedObjectDetails": "Detalhes do objeto rastreado", "type": { "details": "detalhes", "video": "vídeo", "object_lifecycle": "ciclo de vida do objeto", - "snapshot": "snapshot" + "snapshot": "captura de ecrã" }, "objectLifecycle": { "title": "Ciclo de vida do objeto", "lifecycleItemDesc": { "attribute": { "other": "{{label}} reconhecido como {{attribute}}", - "faceOrLicense_plate": "{{attribute}} detectado por {{label}}" + "faceOrLicense_plate": "{{attribute}} detetado por {{label}}" }, "gone": "{{label}} saiu", "heard": "{{label}} ouvido", "visible": "{{label}} detectado", - "external": "{{label}} detectado", + "external": "{{label}} detetado", "entered_zone": "{{label}} entrou em {{zones}}", "active": "{{label}} se tornou ativo", "stationary": "{{label}} se tornou estacionário", @@ -128,7 +133,7 @@ } }, "annotationSettings": { - "title": "Configurações de anotação", + "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.", @@ -140,8 +145,8 @@ } }, "showAllZones": { - "title": "Mostrar todas as zonas", - "desc": "Sempre mostrar zonas nos quadros onde os objetos entraram em uma zona." + "title": "Mostrar Todas as Zonas", + "desc": "Mostrar sempre as zonas nas imagens onde os objetos entraram numa zona." } }, "carousel": { @@ -150,9 +155,9 @@ }, "noImageFound": "Nenhuma imagem encontrada para este carimbo de data/hora.", "createObjectMask": "Criar Máscara de Objeto", - "adjustAnnotationSettings": "Ajustar configurações de anotação", - "autoTrackingTips": "As posições da caixa delimitadora serão imprecisas para câmeras com rastreamento automático.", - "scrollViewTips": "Faça scroll para ver os momentos significativos do ciclo de vida deste 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" }, @@ -183,6 +188,14 @@ }, "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": { @@ -205,5 +218,11 @@ "trackedObjectsCount_one": "{{count}} objeto rastreado ", "trackedObjectsCount_many": "{{count}} objetos rastreados ", "trackedObjectsCount_other": "", - "exploreMore": "Explora mais objetos {{label}}" + "exploreMore": "Explora mais objetos {{label}}", + "aiAnalysis": { + "title": "Análise IA" + }, + "concerns": { + "label": "Preocupações" + } } diff --git a/web/public/locales/pt/views/live.json b/web/public/locales/pt/views/live.json index eb0330a97..770028a85 100644 --- a/web/public/locales/pt/views/live.json +++ b/web/public/locales/pt/views/live.json @@ -42,6 +42,14 @@ "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", @@ -130,7 +138,8 @@ "recording": "Gravando", "audioDetection": "Detecção de áudio", "autotracking": "Rastreamento automático", - "snapshots": "Snapshots" + "snapshots": "Snapshots", + "transcription": "Transcrição de áudio" }, "effectiveRetainMode": { "modes": { @@ -154,5 +163,9 @@ }, "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/settings.json b/web/public/locales/pt/views/settings.json index f453e6a5b..1bab92d78 100644 --- a/web/public/locales/pt/views/settings.json +++ b/web/public/locales/pt/views/settings.json @@ -6,11 +6,13 @@ "motionTuner": "Ajuste de movimento - Frigate", "object": "Depuração - Frigate", "authentication": "Configurações de autenticação - Frigate", - "general": "Configurações Gerais - 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" + "enrichments": "Configurações Avançadas - Frigate", + "cameraManagement": "Gerir Câmaras - Frigate", + "cameraReview": "Configurações de Revisão de Câmara - Frigate" }, "menu": { "ui": "UI", @@ -22,7 +24,11 @@ "users": "Utilizadores", "notifications": "Notificações", "frigateplus": "Frigate+", - "enrichments": "Avançado" + "enrichments": "Avançado", + "triggers": "Gatilhos", + "cameraManagement": "Gestão", + "cameraReview": "Rever", + "roles": "Papéis" }, "dialog": { "unsavedChanges": { @@ -465,6 +471,19 @@ "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": { @@ -499,6 +518,44 @@ "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": { @@ -594,7 +651,8 @@ "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:" + "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}}", @@ -682,5 +740,247 @@ "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 index 2826a9dd9..9f90073c9 100644 --- a/web/public/locales/pt/views/system.json +++ b/web/public/locales/pt/views/system.json @@ -1,14 +1,14 @@ { "documentTitle": { - "storage": "Estatísticas de armazenamento - Frigate", - "general": "Estatísticas gerais - Frigate", - "enrichments": "Estatísticas de enriquecimento - Frigate", + "storage": "Frigate - Estatísticas de Armazenamento", + "general": "Frigate - Estatísticas Gerais", + "enrichments": "Frigate - Estatísticas de Enriquecimento", "logs": { - "frigate": "Logs do Frigate - Frigate", - "go2rtc": "Logs do Go2RTC - Frigate", - "nginx": "Logs do Nginx - Frigate" + "frigate": "Frigate - Registos de Eventos do Frigate", + "go2rtc": "Frigate - Registos de Eventos do Go2RTC", + "nginx": "Frigate - Registos de Eventos do Nginx" }, - "cameras": "Estatísticas das câmaras - Frigate" + "cameras": "Frigate - Estatísticas das Câmaras" }, "title": "Sistema", "metrics": "Métricas do sistema", @@ -16,22 +16,22 @@ "type": { "label": "Tipo", "timestamp": "Carimbo de hora", - "tag": "Tag", + "tag": "Etiqueta", "message": "Mensagem" }, "copy": { - "success": "Logs copiados para a área de transferência", - "label": "Copiar para a área de transferência", - "error": "Não foi possível copiar os logs para a área de transferência" + "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": "Descarregar logs" + "label": "Transferir Registos" }, - "tips": "Os logs estão a ser transmitidos do servidor", + "tips": "Os registos estão a ser transmitidos do servidor", "toast": { "error": { - "fetchingLogsFailed": "Erro ao buscar logs: {{errorMessage}}", - "whileStreamingLogs": "Erro ao transmitir logs: {{errorMessage}}" + "fetchingLogsFailed": "Erro ao obter os registos: {{errorMessage}}", + "whileStreamingLogs": "Erro enquanto transmitia os registos: {{errorMessage}}" } } }, @@ -49,11 +49,15 @@ "title": "Armazenamento da câmara" }, "title": "Armazenamento", - "overview": "Visão geral", + "overview": "Sinopse", "recordings": { "title": "Gravações", "earliestRecording": "Primeira gravação disponível:", - "tips": "Esse valor representa o armazenamento total usado pelas gravações na base de dados do Frigate. O Frigate não acompanha o uso de armazenamento de todos os ficheiros no seu disco." + "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": { @@ -83,19 +87,19 @@ "skipped": "ignorado", "ffmpeg": "FFmpeg", "cameraFfmpeg": "{{camName}} FFmpeg", - "cameraFramesPerSecond": "quadros por segundo de {{camName}}", + "cameraFramesPerSecond": "imagens por segundo de {{camName}}", "cameraCapture": "captura de {{camName}}", - "cameraDetectionsPerSecond": "detecções por segundo de {{camName}}", - "overallFramesPerSecond": "quadros por segundo totais (FPS)", - "overallDetectionsPerSecond": "detecções por segundo totais", - "overallSkippedDetectionsPerSecond": "detecções ignoradas por segundo totais", - "cameraDetect": "detecção de {{camName}}", - "cameraSkippedDetectionsPerSecond": "detecções ignoradas por segundo 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." + "copyToClipboard": "Dados de exploração copiados para a área de transferência." }, "error": { "unableToProbeCamera": "Não foi possível explorar a câmara: {{errorMessage}}" @@ -104,43 +108,45 @@ }, "lastRefreshed": "Última atualização: ", "stats": { - "ffmpegHighCpuUsage": "{{camera}} tem alto uso de CPU FFmpeg ({{ffmpegAvg}}%)", - "detectHighCpuUsage": "{{camera}} tem alto uso de CPU de detecção ({{detectAvg}}%)", + "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á offline", - "detectIsSlow": "{{detect}} está 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": "Detectores", - "cpuUsage": "Utilização do CPU do Detector", - "memoryUsage": "Utilização da memória do Detector", - "inferenceSpeed": "Velocidade de Inferência do Detector", - "temperature": "Temperatura do 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ções de hardware", - "gpuUsage": "Utilização GPU", - "gpuMemory": "Memória GPU", + "title": "Informação de Hardware", + "gpuUsage": "Utilização da GPU", + "gpuMemory": "Memória da GPU", "gpuInfo": { "nvidiaSMIOutput": { - "driver": "Driver: {{driver}}", + "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ções do GPU" + "label": "Copiar informação da GPU" }, "closeInfo": { - "label": "Fechar informações do GPU" + "label": "Fechar informação da GPU" }, "toast": { - "success": "Informações do GPU copiadas para a área de transferência" + "success": "Informação da GPU copiada para a área de transferência" }, "vainfoOutput": { "title": "Saída do Vainfo", @@ -149,32 +155,32 @@ "processError": "Erro no processo:" } }, - "gpuEncoder": "GPU Encoder", - "gpuDecoder": "GPU Decoder", + "gpuEncoder": "Codificador da GPU", + "gpuDecoder": "Descodificador da GPU", "npuUsage": "Utilização NPU", "npuMemory": "Memória NPU" }, "otherProcesses": { - "title": "Outros processos", - "processCpuUsage": "Uso de CPU do processo", - "processMemoryUsage": "Uso de memória do processo" + "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", + "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", + "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 Detecção de Placas YOLOv9", - "yolov9_plate_detection": "Detecção de Placas YOLOv9" + "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 index 8221339db..56815c618 100644 --- a/web/public/locales/ro/audio.json +++ b/web/public/locales/ro/audio.json @@ -425,5 +425,79 @@ "single-lens_reflex_camera": "Cameră reflex cu un singur obiectiv", "fusillade": "descărcare de focuri", "pink_noise": "Zgomot roz", - "field_recording": "Înregistrare pe teren" + "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 index 145d511a4..8d5177776 100644 --- a/web/public/locales/ro/common.json +++ b/web/public/locales/ro/common.json @@ -1,8 +1,8 @@ { "time": { "untilForTime": "Până la {{time}}", - "untilForRestart": "Pana la repornirea Frigate.", - "untilRestart": "Pana la repornire", + "untilForRestart": "Până la repornirea Frigate.", + "untilRestart": "Până la repornire", "ago": "{{timeAgo}} în urmă", "justNow": "Acum", "today": "Astăzi", @@ -42,7 +42,7 @@ "24hour": "dd-MM-yy-HH-mm-ss" }, "30minutes": "30 de minute", - "1hour": "O oră", + "1hour": "1 oră", "12hours": "12 ore", "24hours": "24 de ore", "pm": "PM", @@ -78,7 +78,10 @@ "minute_other": "{{time}} de minute", "second_one": "{{time}} secundă", "second_few": "{{time}} secunde", - "second_other": "{{time}} de secunde" + "second_other": "{{time}} de secunde", + "inProgress": "În desfășurare", + "invalidStartTime": "Oră de început invalidă", + "invalidEndTime": "Oră de sfârșit invalidă" }, "menu": { "documentation": { @@ -123,7 +126,15 @@ "ro": "Română (Română)", "hu": "Magyar (Maghiară)", "fi": "Suomi (Finlandeză)", - "th": "ไทย (Thailandeză)" + "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)" }, "theme": { "default": "Implicit", @@ -171,7 +182,8 @@ }, "withSystem": "Modul sistemului", "restart": "Repornește Frigate", - "review": "Revizuire" + "review": "Revizuire", + "classification": "Clasificare" }, "button": { "cameraAudio": "Sunet cameră", @@ -208,7 +220,8 @@ "unselect": "Deselectează", "export": "Exportă", "deleteNow": "Șterge acum", - "next": "Următorul" + "next": "Următorul", + "continue": "Continuă" }, "unit": { "speed": { @@ -218,10 +231,24 @@ "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" + "back": "Mergi înapoi", + "hide": "Ascunde {{item}}", + "show": "Afișează {{item}}", + "ID": "ID", + "none": "Niciuna", + "all": "Toate", + "other": "Altele" }, "selectItem": "Selectează {{item}}", "pagination": { @@ -261,5 +288,18 @@ "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" } } diff --git a/web/public/locales/ro/components/auth.json b/web/public/locales/ro/components/auth.json index 4fa303853..cc3b5923b 100644 --- a/web/public/locales/ro/components/auth.json +++ b/web/public/locales/ro/components/auth.json @@ -10,6 +10,7 @@ "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 index d93a81dcc..55396367d 100644 --- a/web/public/locales/ro/components/camera.json +++ b/web/public/locales/ro/components/camera.json @@ -66,7 +66,8 @@ "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": "Vedere de ansamblu" } }, "debug": { diff --git a/web/public/locales/ro/components/dialog.json b/web/public/locales/ro/components/dialog.json index c07b2cee0..cbbbf7115 100644 --- a/web/public/locales/ro/components/dialog.json +++ b/web/public/locales/ro/components/dialog.json @@ -46,7 +46,8 @@ "button": { "deleteNow": "Șterge acum", "export": "Exportă", - "markAsReviewed": "Marchează ca revizuit" + "markAsReviewed": "Marchează ca revizuit", + "markAsUnreviewed": "Marchează ca nerevizuit" }, "confirmDelete": { "toast": { @@ -82,12 +83,13 @@ "export": "Exportă", "selectOrExport": "Selectează sau exportă", "toast": { - "success": "Exportul a început cu succes. Vizualizați fișierul în dosarul /exports.", + "success": "Exportul a început cu succes. Vizualizați fișierul pe pagina de exporturi.", "error": { "failed": "Eroare la pornirea exportului: {{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ă" }, "fromTimeline": { "saveExport": "Salvează exportul", @@ -105,7 +107,7 @@ }, "showStats": { "label": "Afișează statistici streaming", - "desc": "Activează această opțiune pentru a afișa statisticile de streaming ca un overlay peste fluxul camerei." + "desc": "Activează această opțiune pentru a afișa statisticile de streaming ca un overlay peste stream-ul camerei." }, "debugView": "Vizualizator depanare" }, @@ -122,5 +124,13 @@ } } } + }, + "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 index 40c0c593c..74a65aa62 100644 --- a/web/public/locales/ro/components/filter.json +++ b/web/public/locales/ro/components/filter.json @@ -121,6 +121,20 @@ "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." + "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/views/classificationModel.json b/web/public/locales/ro/views/classificationModel.json new file mode 100644 index 000000000..1ecc6018e --- /dev/null +++ b/web/public/locales/ro/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "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": "Clasă ștearsă", + "deletedImage": "Imagini șterse", + "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}}" + }, + "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}}" + } + }, + "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 stări lipsă", + "description": "Este recomandat să alegi exemple pentru toate stările pentru rezultate optime. Poți continua fără a selecta toate stările, dar modelul nu va fi antrenat până când toate stările nu au imagini. După continuare, folosește vizualizarea Clasificări recente pentru a clasifica imagini pentru stările lipsă, apoi antrenează modelul." + } + } + }, + "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" +} diff --git a/web/public/locales/ro/views/configEditor.json b/web/public/locales/ro/views/configEditor.json index cecfb7cc7..21f7d4769 100644 --- a/web/public/locales/ro/views/configEditor.json +++ b/web/public/locales/ro/views/configEditor.json @@ -1,5 +1,5 @@ { - "documentTitle": "Editor configurație - Frigate", + "documentTitle": "Editor de configurație - Frigate", "configEditor": "Editor de configurație", "copyConfig": "Copiază setările", "saveAndRestart": "Salvează și repornește", @@ -12,5 +12,7 @@ "savingError": "Eroare la salvarea setărilor" } }, - "confirm": "Ieși fără să salvezi?" + "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 index 30ae1ecb1..f4f2ef120 100644 --- a/web/public/locales/ro/views/events.json +++ b/web/public/locales/ro/views/events.json @@ -8,7 +8,11 @@ "empty": { "alert": "Nu sunt alerte de revizuit", "detection": "Nu sunt detecții de revizuit", - "motion": "Nu au fost găsite date despre mișcare" + "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": "Cronologie", "timeline.aria": "Selectează cronologia", @@ -34,5 +38,30 @@ "detections": "Detecții", "detected": "detectat", "selected_one": "{{count}} selectate", - "selected_other": "{{count}} selectate" + "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" } diff --git a/web/public/locales/ro/views/explore.json b/web/public/locales/ro/views/explore.json index f9b4b0867..d76f9191d 100644 --- a/web/public/locales/ro/views/explore.json +++ b/web/public/locales/ro/views/explore.json @@ -33,7 +33,9 @@ "details": "detalii", "snapshot": "snapshot", "video": "video", - "object_lifecycle": "ciclul de viață al obiectului" + "object_lifecycle": "ciclul de viață al obiectului", + "thumbnail": "miniatură", + "tracking_details": "detalii de urmărire" }, "objectLifecycle": { "lifecycleItemDesc": { @@ -70,7 +72,7 @@ "offset": { "label": "Compensare adnotare", "documentation": "Citește documentația ", - "desc": "Aceste date provin din fluxul de detecție al camerei tale, dar sunt suprapuse pe imaginile din fluxul de înregistrare. Este puțin probabil ca cele două fluxuri 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.", + "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": { @@ -103,12 +105,16 @@ "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." + "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}}" + "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}}" } } }, @@ -153,7 +159,18 @@ }, "expandRegenerationMenu": "Extinde meniul de regenerare", "regenerateFromSnapshot": "Regenerează din snapshot", - "regenerateFromThumbnails": "Regenerează din miniaturi" + "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" + } }, "exploreMore": "Explorează mai multe obiecte cu {{label}}", "trackedObjectDetails": "Detalii despre obiectul urmărit", @@ -187,12 +204,34 @@ "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" } }, "dialog": { "confirmDelete": { "title": "Confirmă ștergerea", - "desc": "Ștergerea acestui obiect urmărit elimină instantaneul, orice încorporări salvate și orice intrări asociate ciclului de viață al obiectului. Materialul video înregistrat al acestui obiect urmărit în vizualizarea Istoric NU va fi șters.

    Ești sigur că vrei să continui?" + "desc": "Ștergerea acestui obiect urmărit elimină snapshot-ul, orice încorporă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?" } }, "noTrackedObjects": "Nu au fost găsite obiecte urmărite", @@ -204,6 +243,63 @@ "error": "Ștergerea obiectului urmărit a eșuat: {{errorMessage}}" } }, - "tooltip": "Potrivire {{type}} cu {{confidence}}%" + "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" + } + }, + "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 index 786b07150..fa9077459 100644 --- a/web/public/locales/ro/views/exports.json +++ b/web/public/locales/ro/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Eroare redenumire export: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Partajează exportul", + "downloadVideo": "Descarcă videoclipul", + "editName": "Editează numele", + "deleteExport": "Șterge exportul" } } diff --git a/web/public/locales/ro/views/faceLibrary.json b/web/public/locales/ro/views/faceLibrary.json index da9261d07..360143d5d 100644 --- a/web/public/locales/ro/views/faceLibrary.json +++ b/web/public/locales/ro/views/faceLibrary.json @@ -1,8 +1,8 @@ { "description": { - "addFace": "Parcurge adăugarea unei colecții noi la biblioteca de fețe.", + "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 poate conține doar litere, cifre, spații, apostrofuri, liniuțe de subliniere și cratime." + "invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe." }, "details": { "person": "Persoană", @@ -20,15 +20,16 @@ "createFaceLibrary": { "desc": "Creează o colecție nouă", "title": "Creează colecție", - "nextSteps": "Pentru a construi o bază solidă:
  • Folosește fila „antrenare” 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.
  • ", + "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": "Antrenează", - "aria": "Selectează antrenarea" + "title": "Recunoașteri Recente", + "aria": "Selectează Recunoașteri Recente", + "titleShort": "Recent" }, "steps": { "description": { @@ -69,7 +70,7 @@ "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.", + "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.", @@ -88,7 +89,7 @@ }, "imageEntry": { "dropActive": "Trage imaginea aici…", - "dropInstructions": "Trage și plasează o imagine aici sau fă clic pentru a selecta", + "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." diff --git a/web/public/locales/ro/views/live.json b/web/public/locales/ro/views/live.json index 39ce37747..7ddaa53d8 100644 --- a/web/public/locales/ro/views/live.json +++ b/web/public/locales/ro/views/live.json @@ -17,9 +17,9 @@ }, "move": { "clickMove": { - "label": "Fă click în cadrul imaginii pentru a centra camera", - "enable": "Activează clic pentru a muta", - "disable": "Dezactivează clic pentru a muta" + "label": "Apasă în cadrul imaginii pentru a centra camera", + "enable": "Activează mutarea prin clic", + "disable": "Dezactivează mutarea prin clic" }, "left": { "label": "Mișcă camera PTZ spre stânga" @@ -36,10 +36,18 @@ }, "frame": { "center": { - "label": "Fă clic în cadru pentru a centra camera PTZ" + "label": "Apasă în cadru pentru a centra camera PTZ" } }, - "presets": "Presetări cameră 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", @@ -78,8 +86,8 @@ "disable": "Ascunde statisticile de streaming" }, "manualRecording": { - "title": "Înregistrare la cerere", - "tips": "Pornește un eveniment manual bazat pe setările de păstrare a înregistrărilor pentru această cameră.", + "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." @@ -92,7 +100,7 @@ "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ă, va fi salvată doar o captură de ecran.", + "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." @@ -126,6 +134,9 @@ "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": { @@ -135,7 +146,8 @@ "recording": "Înregistrare", "snapshots": "Snapshot-uri", "audioDetection": "Detectare sunet", - "autotracking": "Urmărire automată" + "autotracking": "Urmărire automată", + "transcription": "Transcriere audio" }, "history": { "label": "Afișează înregistrările istorice" @@ -154,5 +166,24 @@ "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." + } } } diff --git a/web/public/locales/ro/views/search.json b/web/public/locales/ro/views/search.json index 9e80fdc3b..5c5f391e8 100644 --- a/web/public/locales/ro/views/search.json +++ b/web/public/locales/ro/views/search.json @@ -26,14 +26,15 @@ "max_speed": "Viteza maximă", "recognized_license_plate": "Număr de înmatriculare recunoscut", "has_clip": "Are videoclip", - "has_snapshot": "Are snapshot" + "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 făcând clic pe „X”-ul de lângă ele.", + "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.", diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json index fad64bd91..a85f852cd 100644 --- a/web/public/locales/ro/views/settings.json +++ b/web/public/locales/ro/views/settings.json @@ -8,9 +8,11 @@ "notifications": "Setări notificări - Frigate", "motionTuner": "Ajustare mișcare - Frigate", "object": "Depanare - Frigate", - "general": "Setări generale - Frigate", + "general": "Setări interfață - Frigate", "frigatePlus": "Setări Frigate+ - Frigate", - "enrichments": "Setări de Îmbogățiri - Frigate" + "enrichments": "Setări de Îmbogățiri - Frigate", + "cameraManagement": "Gestionează Camerele - Frigate", + "cameraReview": "Setări Revizuire Cameră - Frigate" }, "menu": { "ui": "Interfață utilizator", @@ -21,7 +23,11 @@ "debug": "Depanare", "users": "Utilizatori", "notifications": "Notificări", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "triggers": "Declanșatoare", + "roles": "Roluri", + "cameraManagement": "Administrare", + "cameraReview": "Revizuire" }, "dialog": { "unsavedChanges": { @@ -34,7 +40,7 @@ "noCamera": "Nicio cameră" }, "general": { - "title": "Setări generale", + "title": "Setări interfață", "liveDashboard": { "title": "Tabloul de bord live", "automaticLiveView": { @@ -44,6 +50,14 @@ "playAlertVideos": { "label": "Redă videoclipurile de alertă", "desc": "În mod implicit, alertele recente din panoul Live se redau ca videoclipuri mici, ce ruleaza repetat. Dezactivează această opțiune pentru a afișa doar o imagine statică a alertelor recente pe acest dispozitiv/browser." + }, + "displayCameraNames": { + "label": "Afișează întotdeauna numele camerelor", + "desc": "Afișează întotdeauna numele camerelor într-un indicator în tabloul de bord cu vizualizare live pe mai multe camere." + }, + "liveFallbackTimeout": { + "label": "Timp de expirare pentru redarea live", + "desc": "Când stream-ul live de înaltă calitate al unei camere nu este disponibil, revino la modul cu lățime de bandă scăzută după acest număr de secunde. Implicit: 3." } }, "storedLayouts": { @@ -177,6 +191,44 @@ "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": { @@ -206,7 +258,7 @@ "name": { "inputPlaceHolder": "Introdu un nume…", "title": "Nume", - "tips": "Numele trebuie să aibă cel puțin 2 caractere și nu trebuie să fie identic cu numele unei camere sau al unei alte zone existente." + "tips": "Numele trebuie să aibă cel puțin 2 caractere, să conțină cel puțin o literă și să nu fie numele unei camere sau al unei alte zone din această cameră." }, "inertia": { "title": "Inerție", @@ -223,9 +275,9 @@ "desc": "Specifică o viteză minimă pe care trebuie să o aibă obiectele pentru a fi considerate în această zonă." }, "documentTitle": "Editează zone - Frigate", - "clickDrawPolygon": "Click pentru a desena un poligon pe imagine.", + "clickDrawPolygon": "Apasă pentru a desena un poligon pe imagine.", "toast": { - "success": "Zona ({{zoneName}}) a fost salvată. Repornește Frigate pentru a aplica modificările." + "success": "Zona ({{zoneName}}) a fost salvată." }, "label": "Zone", "objects": { @@ -238,7 +290,7 @@ "point_one": "{{count}} punct", "point_few": "{{count}} puncte", "point_other": "{{count}} de puncte", - "clickDrawPolygon": "Fă click pentru a desena un poligon pe imagine.", + "clickDrawPolygon": "Fă clic pentru a desena un poligon pe imagine.", "label": "Măști de mișcare", "documentTitle": "Editează masca de mișcare - Frigate", "desc": { @@ -253,8 +305,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} a fost salvat. Repornește Frigate pentru a aplica modificările.", - "noName": "Masca de mișcare a fost salvată.Repornește Frigate pentru a aplica modificările." + "title": "{{polygonName}} a fost salvat.", + "noName": "Masca de mișcare a fost salvată." } }, "polygonAreaTooLarge": { @@ -282,11 +334,11 @@ }, "toast": { "success": { - "noName": "Masca de obiecte a fost salvată. Repornește Frigate pentru a aplica modificările.", - "title": "{{polygonName}} a fost salvat. Repornește Frigate pentru a aplica modificările." + "noName": "Masca de obiecte a fost salvată.", + "title": "{{polygonName}} a fost salvat." } }, - "clickDrawPolygon": "Fă click pentru a desena un poligon pe imagine.", + "clickDrawPolygon": "Fă clic pentru a desena un poligon pe imagine.", "context": "Măștile de filtrare a obiectelor sunt folosite pentru a elimina falsele pozitive pentru un anumit tip de obiect, în funcție de locația acestuia." }, "restart_required": "Repornire necesară (măști/zone modificate)", @@ -310,7 +362,8 @@ "mustNotContainPeriod": "Numele zonei nu trebuie să conțină puncte.", "hasIllegalCharacter": "Numele zonei conține caractere nepermise.", "mustNotBeSameWithCamera": "Numele zonei nu trebuie să fie identic cu numele camerei.", - "alreadyExists": "O zonă cu acest nume există deja pentru această cameră." + "alreadyExists": "O zonă cu acest nume există deja pentru această cameră.", + "mustHaveAtLeastOneLetter": "Numele zonei trebuie să aibă cel puțin o literă." } }, "polygonDrawing": { @@ -399,7 +452,20 @@ "zones": { "title": "Zone", "desc": "Afișează conturul oricăror zone definite" - } + }, + "paths": { + "title": "Căi", + "desc": "Afișează punctele semnificative ale traseului obiectului urmărit", + "tips": "

    Căi


    Liniile și cercurile vor indica punctele semnificative prin care obiectul urmărit s-a deplasat pe parcursul ciclului său de viață.

    " + }, + "audio": { + "title": "Audio", + "noAudioDetections": "Nicio detecție audio", + "score": "scor", + "currentRMS": "RMS curent", + "currentdbFS": "dbFS curent" + }, + "openCameraWebUI": "Deschide interfața web pentru {{camera}}" }, "users": { "dialog": { @@ -415,7 +481,8 @@ "admin": "Administrator", "adminDesc": "Acces complet la toate funcțiile.", "viewer": "Vizualizator", - "viewerDesc": "Limitat doar la tablourile de bord Live, Revizuire, Explorare și Exporturi." + "viewerDesc": "Limitat doar la tablourile de bord Live, Revizuire, Explorare și Exporturi.", + "customDesc": "Rol personalizat cu acces specific la cameră." }, "select": "Selectează un rol", "title": "Schimbă rolul utilizatorului" @@ -436,7 +503,16 @@ }, "title": "Parolă", "match": "Parolele se potrivesc", - "notMatch": "Parolele nu se potrivesc" + "notMatch": "Parolele nu se potrivesc", + "show": "Afișează parola", + "hide": "Ascunde parola", + "requirements": { + "title": "Cerințe parolă:", + "length": "Cel puțin 8 caracter", + "uppercase": "Cel puțin o literă majusculă", + "digit": "Cel puțin o cifră", + "special": "Cel puțin un caracter special (!@#$%^&*(),.?\":{}|<>)" + } }, "passwordIsRequired": "Este nevoie de parolă", "user": { @@ -451,7 +527,11 @@ "placeholder": "Re-introdu parola nouă" } }, - "usernameIsRequired": "Este nevoie de numele de utilizator" + "usernameIsRequired": "Este nevoie de numele de utilizator", + "currentPassword": { + "title": "Parola curentă", + "placeholder": "Introduceți parola curentă" + } }, "createUser": { "confirmPassword": "Te rog să confirmi parola", @@ -464,7 +544,12 @@ "doNotMatch": "Parolele nu se potrivesc", "updatePassword": "Actualizează parola pentru {{username}}", "setPassword": "Schimbă parola", - "desc": "Creează o parolă puternică pentru a securiza acest cont." + "desc": "Creează o parolă puternică pentru a securiza acest cont.", + "currentPasswordRequired": "Parola curentă este obligatorie", + "incorrectCurrentPassword": "Parola curentă incorectă", + "passwordVerificationFailed": "Nu s-a putut verifica parola", + "multiDeviceWarning": "Orice alte dispozitive pe care ești autentificat vor trebui să se autentifice din nou în termen de {{refresh_time}}.", + "multiDeviceAdmin": "De asemenea, poți forța toți utilizatorii să se reautentifice imediat prin rotirea secretului tău JWT." } }, "addUser": "Adaugă utilizator", @@ -486,7 +571,7 @@ "deleteUserFailed": "Ștergerea utilizatorului a eșuat: {{errorMessage}}" } }, - "updatePassword": "Actualizează parola", + "updatePassword": "Resetează parola", "title": "Utilizatori", "table": { "username": "Nume utilizator", @@ -495,7 +580,7 @@ "noUsers": "Nu a fost găsit niciun utilizator.", "changeRole": "Schimbă rolul utilizatorului", "deleteUser": "Șterge utilizatorul", - "password": "Parolă" + "password": "Resetează parola" } }, "notification": { @@ -619,5 +704,547 @@ "success": "Setările de mișcare au fost salvate." }, "title": "Reglaj detecție mișcare" + }, + "triggers": { + "documentTitle": "Declanșatoare", + "management": { + "title": "Declanșatoare", + "desc": "Gestionează declanșatoarele pentru {{camera}}. Folosește tipul miniatură pentru a declanșa pe miniaturi similare cu obiectul urmărit selectat și tipul descriere pentru a declanșa pe descrieri similare textului pe care îl specifici." + }, + "addTrigger": "Adaugă declanșator", + "table": { + "name": "Nume", + "type": "Tip", + "content": "Conținut", + "threshold": "Prag", + "actions": "Acțiuni", + "noTriggers": "Nu sunt configurate declanșatoare pentru această cameră.", + "edit": "Editează", + "deleteTrigger": "Elimină declanșatorul", + "lastTriggered": "Ultima declanșare" + }, + "type": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "actions": { + "alert": "Marchează ca alertă", + "notification": "Trimite notificare", + "sub_label": "Adaugă subeticheta", + "attribute": "Adaugă atribut" + }, + "dialog": { + "createTrigger": { + "title": "Crează declanșator", + "desc": "Creează un declanșator pentru camera {{camera}}" + }, + "editTrigger": { + "title": "Editează declanșatorul", + "desc": "Editează setările pentru declanșatorul de pe camera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimină declanșatorul", + "desc": "Ești sigur că vrei să ștergi declanșatorul {{triggerName}}? Această acțiune nu poate fi anulată." + }, + "form": { + "name": { + "title": "Nume", + "placeholder": "Denumește acest declanșator", + "error": { + "minLength": "Câmpul trebuie să aibă cel puțin 2 caractere.", + "invalidCharacters": "Câmpul poate conține doar litere, cifre, underscore-uri și cratime.", + "alreadyExists": "Un declanșator cu acest nume există deja pentru această cameră." + }, + "description": "Introduceți un nume sau o descriere unică pentru a identifica acest declanșator" + }, + "enabled": { + "description": "Activează sau dezactivează acest declanșator" + }, + "type": { + "title": "Tip", + "placeholder": "Selectează tipul de declanșator", + "description": "Declanșează atunci când este detectată o descriere de obiect urmărit similară", + "thumbnail": "Declanșează atunci când este detectată o miniatură de obiect urmărit similară" + }, + "content": { + "title": "Conținut", + "imagePlaceholder": "Selectează o miniatură", + "textPlaceholder": "Introdu conținutul textului", + "imageDesc": "Sunt afișate doar ultimele 100 de miniaturi. Dacă nu găsiți miniatura dorită, vă rugăm să verificați obiectele anterioare în Explorator și să configurați un declanșator din meniul de acolo.", + "textDesc": "Introduceți textul pentru a declanșa această acțiune atunci când este detectată o descriere de obiect urmărit similară.", + "error": { + "required": "Conținutul este obligatoriu." + } + }, + "threshold": { + "title": "Prag", + "error": { + "min": "Pragul trebuie să fie cel puțin 0", + "max": "Pragul trebuie să fie cel mult 1" + }, + "desc": "Setați pragul de similitudine pentru acest declanșator. Un prag mai mare înseamnă că este necesară o potrivire mai apropiată pentru declanșarea acestuia." + }, + "actions": { + "title": "Acțiuni", + "desc": "În mod implicit, Frigate trimite un mesaj MQTT pentru toate declanșatoarele. Subetichetele adaugă numele declanșatorului la eticheta obiectului. Atributele sunt metadate căutabile, stocate separat în metadatele obiectului urmărit.", + "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": "Declanșatorul {{name}} a fost creat cu succes.", + "updateTrigger": "Declanșatorul {{name}} a fost actualizat cu succes.", + "deleteTrigger": "Declanșatorul {{name}} a fost eliminat cu succes." + }, + "error": { + "createTriggerFailed": "Crearea declanșatorului a eșuat: {{errorMessage}}", + "updateTriggerFailed": "Actualizarea declanșatorului a eșuat: {{errorMessage}}", + "deleteTriggerFailed": "Eliminarea declanșatorului a eșuat: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Căutarea semantică este dezactivată", + "desc": "Căutarea semantică trebuie să fie activată pentru a utiliza declanșatoarele." + }, + "wizard": { + "title": "Creează declanșator", + "step1": { + "description": "Configurează setările de bază pentru declanșatorul tău." + }, + "step2": { + "description": "Configurează conținutul care va declanșa această acțiune." + }, + "step3": { + "description": "Configurează pragul și acțiunile pentru acest declanșator." + }, + "steps": { + "nameAndType": "Nume și Tip", + "configureData": "Configurează datele", + "thresholdAndActions": "Prag și Acțiuni" + } + } + }, + "roles": { + "management": { + "title": "Gestionare rol vizualizator", + "desc": "Gestionează rolurile personalizate de vizualizator și permisiunile lor de acces la cameră pentru această instanță Frigate." + }, + "addRole": "Adaugă rol", + "table": { + "role": "Rol", + "cameras": "Camere", + "actions": "Acțiuni", + "noRoles": "Nu au fost găsite roluri personalizate.", + "editCameras": "Editează camerele", + "deleteRole": "Șterge rol" + }, + "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 „vizualizatori”, care are acces la toate camerele.", + "userRolesUpdated_other": "{{count}} de utilizatori atribuiți acestui rol au fost actualizați la „vizualizatori”, care are acces la toate camerele." + }, + "error": { + "createRoleFailed": "Crearea rolului a eșuat: {{errorMessage}}", + "updateCamerasFailed": "Actualizarea camerelor a eșuat: {{errorMessage}}", + "deleteRoleFailed": "Ștergerea rolului a eșuat: {{errorMessage}}", + "userUpdateFailed": "Actualizarea rolurilor utilizatorilor a eșuat: {{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 nu poate fi anulată. Aceasta va șterge permanent rolul și va atribui orice utilizatori cu acest rol la rolul „vizualizator”, care va oferi acces vizualizator la toate camerele.", + "warn": "Ești sigur că vrei să ștergi {{role}}?", + "deleting": "Se șterge..." + }, + "form": { + "role": { + "title": "Nume rol", + "placeholder": "Introduceți numele rolului", + "desc": "Sunt permise doar litere, cifre, puncte și linii de subliniere.", + "roleIsRequired": "Numele rolului este obligatoriu", + "roleOnlyInclude": "Numele rolului poate include doar litere, cifre, . sau _", + "roleExists": "Un rol cu acest nume există deja." + }, + "cameras": { + "title": "Camere", + "desc": "Selectați camerele la care acest rol are acces. 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ă la sistemul tău Frigate.", + "steps": { + "nameAndConnection": "Nume și Conexiune", + "streamConfiguration": "Configurare streaming", + "validationAndTesting": "Validare și Testare", + "probeOrSnapshot": "Sondează sau fă snapshot" + }, + "save": { + "success": "Camera nouă {{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 să furnizezi un URL de streaming valid", + "testFailed": "Testul de streaming a eșuat: {{error}}" + }, + "step1": { + "description": "Introduceți detaliile camerei și alegeți să testați camera sau să selectați manual marca.", + "cameraName": "Nume cameră", + "cameraNamePlaceholder": "ex. usă_intrare sau Vedere Curte Spate", + "host": "Gazdă/Adresă IP", + "port": "Port", + "username": "Nume de utilizator", + "usernamePlaceholder": "Opțional", + "password": "Parolă", + "passwordPlaceholder": "Opțional", + "selectTransport": "Selectează protocolul de transport", + "cameraBrand": "Brand cameră", + "selectBrand": "Selectează marca camerei pentru șablonul de URL", + "customUrl": "URL Streaming Personalizat", + "brandInformation": "Informații despre brand", + "brandUrlFormat": "Pentru camere cu formatul URL RTSP ca: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://utilizator:parolă@gazdă: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": "Ori selectează un brand de cameră cu adresă gazdă/IP, ori alege „Alta” cu un URL personalizat", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să aibă 64 de caractere sau mai puțin", + "invalidCharacters": "Numele camerei conține caractere nevalide", + "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://\". Este necesară configurare manuală pentru stream-urile de cameră non-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": "Metoda de detecție stream", + "onvifPort": "Port ONVIF", + "probeMode": "Sondare cameră", + "manualMode": "Selecție manuală", + "detectionMethodDescription": "Sondează camera cu ONVIF (dacă este suportat) pentru a găsi URL-urile de stream ale camerei, sau selectează manual marca camerei pentru a utiliza URL-uri predefinite. Pentru a introduce un URL RTSP personalizat, alege metoda manuală și selectează \"Altele\".", + "onvifPortDescription": "Pentru camerele care suportă ONVIF, acesta este de obicei 80 sau 8080.", + "useDigestAuth": "Utilizați autentificarea digest", + "useDigestAuthDescription": "Utilizați autentificarea HTTP digest pentru ONVIF. Unele camere pot necesita un nume de utilizator/parolă ONVIF dedicat în locul utilizatorului standard de administrare." + }, + "step2": { + "description": "Testează camera pentru fluxurile disponibile sau configurează setările manuale pe baza metodei de detectare selectate.", + "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 fost realizat cu succes!", + "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 sondează camera...", + "retry": "Reîncercare", + "testing": { + "probingMetadata": "Se sondează metadatele camerei...", + "fetchingSnapshot": "Se aduce snapshot cameră..." + }, + "probeFailed": "Sondarea camerei a eșuat: {{error}}", + "probingDevice": "Se sondează dispozitivul...", + "probeSuccessful": "Sondare reușită", + "probeError": "Eroare la sondare", + "probeNoSuccess": "Sondare nereușită", + "deviceInfo": "Informații dispozitiv", + "manufacturer": "Producător", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiluri", + "ptzSupport": "Suport PTZ", + "autotrackingSupport": "Suport autourmărire", + "presets": "Presetări", + "rtspCandidates": "Candidați RTSP", + "rtspCandidatesDescription": "Următoarele URL-uri RTSP au fost găsite în urma sondării camerei. Testați conexiunea pentru a vizualiza metadatele stream-ului.", + "noRtspCandidates": "Nu au fost găsite URL-uri RTSP de la cameră. Este posibil ca datele dumneavoastră de autentificare să fie incorecte, sau este posibil ca aparatul foto să nu suporte ONVIF sau metoda utilizată pentru a prelua URL-urile RTSP. Întoarceți-vă și introduceți URL-ul RTSP manual.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Folosește", + "uriCopy": "Copiază", + "uriCopied": "URI copiat în clipboard", + "testConnection": "Testează conexiunea", + "toggleUriView": "Click pentru a comuta vizualizarea URI completă", + "errors": { + "hostRequired": "Gazdă/adresaIP este necesară" + } + }, + "step3": { + "description": "Configurează rolurile stream-ului ș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 {{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ă stream", + "addAnotherStream": "Adaugă alt stream", + "streamUrl": "URL stream", + "streamUrlPlaceholder": "rtsp://utilizator:parolă@adresaIP:port/cale", + "selectStream": "Selectați un flux", + "searchCandidates": "Căutați 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 Obiect", + "record": "Înregistrare", + "audio": "Audio" + }, + "testStream": "Testează conexiunea", + "testSuccess": "Testul stream-ului a avut succes!", + "testFailed": "Testul stream-ului a eșuat", + "testFailedTitle": "Testul a eșuat", + "connected": "Conectat", + "notConnected": "Neconectat", + "featuresTitle": "Funcționalități", + "go2rtc": "Reduceți conexiunile la cameră", + "detectRoleWarning": "Cel puțin un stream trebuie să aibă rolul \"detect\" pentru a continua.", + "rolesPopover": { + "title": "Roluri stream", + "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 audio." + }, + "featuresPopover": { + "title": "Funcționalități stream", + "description": "Utilizați go2rtc restreaming pentru a reduce conexiunile la cameră." + } + }, + "step4": { + "description": "Validare finală și analiză înainte de a salva noua cameră. Conectați fiecare stream înainte de a salva.", + "validationTitle": "Validare stream", + "connectAllStreams": "Conectează toate stream-urile", + "reconnectionSuccess": "Reconectare reușită.", + "reconnectionPartial": "Unele stream-uri nu au reușit să se reconecteze.", + "streamUnavailable": "Previzualizare flux indisponibilă", + "reload": "Reîncarcă", + "connecting": "Conectare...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Eșuat", + "notTested": "Netestat", + "connectStream": "Conectare", + "connectingStream": "Se conectează", + "disconnectStream": "Deconectare", + "estimatedBandwidth": "Lățime de bandă estimată", + "roles": "Roluri", + "ffmpegModule": "Utilizează modul de compatibilitate stream", + "ffmpegModuleDescription": "Dacă stream-ul nu se încarcă după câteva încercări, activați această opțiune. Când este activată, Frigate va utiliza modulul ffmpeg cu go2rtc. Acest lucru poate oferi o compatibilitate mai bună cu unele stream-uri de cameră.", + "none": "Niciuna", + "error": "Eroare", + "streamValidated": "Stream-ul {{number}} validat cu succes", + "streamValidationFailed": "Validarea stream-ului {{number}} a eșuat", + "saveAndApply": "Salvează camera nouă", + "saveError": "Configurație nevalidă. Vă rugăm să vă verificați setările.", + "issues": { + "title": "Validare stream", + "videoCodecGood": "Codecul video: {{codec}}.", + "audioCodecGood": "Codecul audio: {{codec}}.", + "resolutionHigh": "O rezoluție de {{resolution}} poate cauza o utilizare crescută a resurselor.", + "resolutionLow": "O rezoluție de {{resolution}} ar putea fi prea mică pentru detectarea fiabilă a obiectelor mici.", + "noAudioWarning": "Nu a fost detectat audio pentru acest stream, înregistrările nu vor avea audio.", + "audioCodecRecordError": "Codec-ul audio AAC este necesar pentru a suporta audio în înregistrări.", + "audioCodecRequired": "Este necesar un stream audio pentru a suporta detecția audio.", + "restreamingWarning": "Reducerea conexiunilor la cameră pentru stream-ul de înregistrare poate crește ușor utilizarea procesorului (CPU).", + "brands": { + "reolink-rtsp": "RTSP Reolink nu este recomandat. Activați HTTP în setările de firmware ale camerei și reporniți asistentul.", + "reolink-http": "Stream-urile HTTP Reolink ar trebui să folosească FFmpeg pentru o compatibilitate mai bună. Activează 'Use stream compatibility mode' pentru acest stream." + }, + "dahua": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Dahua / Amcrest / EmpireTech suportă stream-uri secundare suplimentare care trebuie activate în setările camerei. Se recomandă să verificați și să utilizați aceste stream-uri dacă sunt disponibile." + }, + "hikvision": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Hikvision suportă stream-uri secundare suplimentare care trebuie activate în setările camerei. Se recomandă să verificați și să utilizați aceste stream-uri dacă sunt disponibile." + } + } + } + }, + "cameraManagement": { + "title": "Administrează Camerele", + "addCamera": "Adaugă cameră nouă", + "editCamera": "Editează cameră:", + "selectCamera": "Selectează o cameră", + "backToSettings": "Înapoi la setările camerei", + "streams": { + "title": "Activează / dezactivează camere", + "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." + }, + "cameraConfig": { + "add": "Adaugă cameră", + "edit": "Editează cameră", + "description": "Configurează setările camerei, inclusiv intrările și rolurile de streaming.", + "name": "Nume cameră", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să fie mai scurt de 64 de caractere.", + "namePlaceholder": "ex. ușă_intrare sau Vedere Curte Spate", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale streaming", + "pathRequired": "Calea streaming este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detectare, înregistrare) poate fi atribuit unui singur stream", + "addInput": "Adaugă stream de intrare", + "removeInput": "Elimină stream-ul de intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "go2rtcStreams": "Streamuri go2rtc", + "streamUrls": "URL-uri streaming", + "addUrl": "Adaugă URL", + "addGo2rtcStream": "Adaugă stream go2rtc", + "toast": { + "success": "Camera {{cameraName}} salvată cu succes" + } + } + }, + "cameraReview": { + "title": "Setări de Revizuire a Camerei", + "object_descriptions": { + "title": "Descrieri de Obiecte cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile de obiecte cu AI Generativ pentru această cameră. Când este dezactivată, descrierile generate de AI nu vor fi solicitate pentru obiectele urmărite pe această cameră." + }, + "review_descriptions": { + "title": "Descrieri de revizuire cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile de revizuire cu AI Generativ pentru această cameră. Când este dezactivat, descrierile generate de AI nu vor fi solicitate pentru elementele de revizuire de pe această cameră." + }, + "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 elemente de revizuire noi. ", + "alerts": "Alerte ", + "detections": "Detecții " + }, + "reviewClassification": { + "title": "Clasificare revizuire", + "desc": "Frigate clasifică elementele de revizuire ca Alerte și Detecții. În mod implicit, toate obiectele de tip persoană și mașină sunt considerate Alerte. Poți rafina clasificarea elementelor tale de revizuire prin configurarea zonelor necesare pentru acestea.", + "noDefinedZones": "Nu sunt definite zone pentru această cameră.", + "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor fi afișate ca Alerte.", + "zoneObjectAlertsTips": "Toate obiectele {{alertsLabels}} detectate în {{zone}} pe {{cameraName}} vor fi afișate ca Alerte.", + "objectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorizate pe {{cameraName}} vor fi afișate ca Detecții indiferent de zona în care se află.", + "zoneObjectDetectionsTips": { + "text": "Toate obiectele {{detectionsLabels}} necategorizate în {{zone}} pe {{cameraName}} vor fi afișate ca Detecții.", + "notSelectDetections": "Toate obiectele {{detectionsLabels}} detectate în {{zone}} pe {{cameraName}} și necategorizate ca Alerte vor fi afișate ca Detecții indiferent de zona în care se află.", + "regardlessOfZoneObjectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorizate pe {{cameraName}} vor fi afișate ca Detecții indiferent de zona în care se află." + }, + "unsavedChanges": "Setări de Clasificare Revizuire nesalvate pentru {{camera}}", + "selectAlertsZones": "Selectați zonele pentru Alerte", + "selectDetectionsZones": "Selectați zonele pentru Detecții", + "limitDetections": "Limitați detecțiile la zone specifice", + "toast": { + "success": "Configurația Clasificare Revizuire a fost salvată. Reporniți Frigate pentru a aplica modificările." + } + } } } diff --git a/web/public/locales/ro/views/system.json b/web/public/locales/ro/views/system.json index 5ba80df9c..6966f124f 100644 --- a/web/public/locales/ro/views/system.json +++ b/web/public/locales/ro/views/system.json @@ -42,6 +42,11 @@ "closeInfo": { "label": "Închide informațiile GPU" } + }, + "intelGpuWarning": { + "title": "Avertisment statistici GPU Intel", + "message": "Statistici GPU indisponibile", + "description": "Aceasta este o eroare cunoscută în instrumentele Intel pentru raportarea statisticilor GPU (intel_gpu_top), unde acestea se blochează și returnează repetat o utilizare GPU de 0%, chiar și în cazurile în care accelerarea hardware și detectarea obiectelor rulează corect pe (i)GPU. Aceasta nu este o eroare Frigate. Poți reporni gazda pentru a remedia temporar problema și pentru a confirma că GPU-ul funcționează corect. Aceasta nu afectează performanța." } }, "detector": { @@ -49,12 +54,20 @@ "title": "Detectori", "cpuUsage": "Utilizarea procesorului", "inferenceSpeed": "Viteza de inferență", - "memoryUsage": "Utilizare memorie detector" + "memoryUsage": "Utilizare memorie detector", + "cpuUsageInformation": "Procesorul utilizat pentru pregătirea datelor de intrare și ieșire către/dinspre modelele de detecție. Această valoare nu măsoară utilizarea în timpul inferenței, chiar dacă este folosit un GPU sau un accelerator." }, "otherProcesses": { "title": "Alte Procese", "processCpuUsage": "Utilizare CPU", - "processMemoryUsage": "Utilizare memorie" + "processMemoryUsage": "Utilizare memorie", + "series": { + "go2rtc": "go2rtc", + "recording": "înregistrare", + "review_segment": "segment de revizuire", + "embeddings": "înglobări", + "audio_detector": "detector audio" + } }, "title": "General" }, @@ -77,7 +90,12 @@ }, "bandwidth": "Lățime de bandă" }, - "overview": "Prezentare generală" + "overview": "Prezentare generală", + "shm": { + "title": "Alocare SHM (memorie partajată)", + "warning": "Dimensiunea curentă a SHM de {{total}}MB este prea mică. Măriți-o la cel puțin {{min_shm}}MB.", + "readTheDocumentation": "Citește documentația" + } }, "title": "Sistem", "logs": { @@ -115,17 +133,27 @@ "face_recognition_speed": "Viteză recunoaștere facială", "plate_recognition_speed": "Viteză recunoaștere numere de înmatriculare", "face_embedding_speed": "Viteză încorporare fețe", - "yolov9_plate_detection_speed": "Viteza detectării numerelor de înmatriculare YOLOv9", + "yolov9_plate_detection_speed": "Viteza detecției numerelor de înmatriculare YOLOv9", "text_embedding_speed": "Viteză încorporare text", - "yolov9_plate_detection": "Detectare numere de înmatriculare YOLOv9" + "yolov9_plate_detection": "Detectare numere de înmatriculare 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": "{{name}} Clasificare", + "classification_speed": "{{name}} Viteză de clasificare", + "classification_events_per_second": "{{name}} Evenimente de clasificare pe secundă" }, - "infPerSecond": "Inferențe pe secundă" + "infPerSecond": "Inferențe pe secundă", + "averageInf": "Timp Mediu de Inferență" }, "cameras": { "info": { "codec": "Codec:", "resolution": "Rezoluție:", - "cameraProbeInfo": "Informații sondare cameră {{camera}}", + "cameraProbeInfo": "Informații testare cameră {{camera}}", "streamDataFromFFPROBE": "Datele stream-ului sunt obținute cu ffprobe.", "aspectRatio": "raport aspect", "fetching": "Se preiau datele camerei", @@ -134,7 +162,7 @@ "audio": "Sunet:", "error": "Eroare:{{error}}", "tips": { - "title": "Informații sondă cameră" + "title": "Informații test cameră" }, "fps": "Cadre/s:", "unknown": "Necunoscut" @@ -160,10 +188,10 @@ "framesAndDetections": "Cadre / Detecții", "toast": { "success": { - "copyToClipboard": "Datele sondei au fost copiate." + "copyToClipboard": "Datele testului au fost copiate." }, "error": { - "unableToProbeCamera": "Sondarea camerei nu a fost posibilă: {{errorMessage}}" + "unableToProbeCamera": "Testarea camerei nu a fost posibilă: {{errorMessage}}" } } }, @@ -174,7 +202,8 @@ "detectHighCpuUsage": "Camera {{camera}} are o utilizare ridicată a procesorului pentru detecție ({{detectAvg}}%)", "ffmpegHighCpuUsage": "Camera {{camera}} are o utilizare ridicată a procesorului FFmpeg ({{ffmpegAvg}}%)", "cameraIsOffline": "{{camera}} este offline", - "healthy": "Sistemul funcționează normal" + "healthy": "Sistemul funcționează normal", + "shmTooLow": "Alocarea /dev/shm ({{total}} MB) ar trebui mărită la cel puțin {{min}} MB." }, "lastRefreshed": "Ultima reîmprospătare: " } diff --git a/web/public/locales/ru/audio.json b/web/public/locales/ru/audio.json index 9f5e58530..e9e6bfc21 100644 --- a/web/public/locales/ru/audio.json +++ b/web/public/locales/ru/audio.json @@ -315,7 +315,7 @@ "slam": "Хлопок", "knock": "Стук", "tap": "Небольшой стук", - "squeak": "Скрип", + "squeak": "Писк", "cupboard_open_or_close": "Открытие или закрытие шкафа", "drawer_open_or_close": "Открытие или закрытие ящика", "dishes": "Тарелки", @@ -425,5 +425,79 @@ "pink_noise": "Розовый шум", "hammer": "Молоток", "firecracker": "Петарда", - "television": "Телевидение" + "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 index 92ee6cf94..0de87d293 100644 --- a/web/public/locales/ru/common.json +++ b/web/public/locales/ru/common.json @@ -87,9 +87,12 @@ "formattedTimestampMonthDayYear": { "12hour": "d MMM, yyyy", "24hour": "d MMM, yyyy" - } + }, + "inProgress": "В процессе", + "invalidStartTime": "Некорректное время начала", + "invalidEndTime": "Некорректное время окончания" }, - "selectItem": "Выбор {{item}}", + "selectItem": "Выбрать {{item}}", "button": { "apply": "Применить", "done": "Готово", @@ -125,10 +128,16 @@ "unselect": "Снять выбор", "export": "Экспортировать", "deleteNow": "Удалить сейчас", - "next": "Следующий" + "next": "Следующий", + "continue": "Продолжить" }, "label": { - "back": "Вернуться" + "back": "Вернуться", + "hide": "Скрыть {{item}}", + "show": "Показать {{item}}", + "ID": "ID", + "all": "Все", + "none": "Ничего" }, "unit": { "speed": { @@ -138,6 +147,14 @@ "length": { "meters": "метры", "feet": "футы" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/час", + "mbph": "МБ/час", + "gbph": "ГБ/час" } }, "menu": { @@ -182,7 +199,15 @@ }, "yue": "粵語 (Кантонский)", "th": "ไทย (Тайский)", - "ca": "Català (Каталонский)" + "ca": "Català (Каталонский)", + "ptBR": "Português brasileiro (Бразильский португальский)", + "sr": "Српски (Сербский)", + "sl": "Slovenščina (Словенский)", + "lt": "Lietuvių (Литовский)", + "bg": "Български (Болгарский)", + "gl": "Galego (Галисийский)", + "id": "Bahasa Indonesia (Индонезийский)", + "ur": "اردو (Урду)" }, "darkMode": { "withSystem": { @@ -232,7 +257,8 @@ "logout": "Выход", "setPassword": "Установить пароль" }, - "appearance": "Внешний вид" + "appearance": "Внешний вид", + "classification": "Распознование" }, "pagination": { "label": "пагинация", @@ -271,5 +297,18 @@ "admin": "Администратор", "viewer": "Наблюдатель", "desc": "Администраторы имеют полный доступ ко всем функциям в интерфейсе Frigate. Наблюдатели ограничены просмотром камер, элементов просмотра и архивных записей." + }, + "readTheDocumentation": "Читать документацию", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} и {{1}}", + "many": "{{items}}, и {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необязательный", + "internalID": "Внутренний идентификатор Frigate, используемый в конфигурации и базе данных" } } diff --git a/web/public/locales/ru/components/auth.json b/web/public/locales/ru/components/auth.json index b227af835..17b983914 100644 --- a/web/public/locales/ru/components/auth.json +++ b/web/public/locales/ru/components/auth.json @@ -10,6 +10,7 @@ "loginFailed": "Ошибка входа", "unknownError": "Неизвестная ошибка. Проверьте логи.", "webUnknownError": "Неизвестная ошибка. Проверьте логи консоли." - } + }, + "firstTimeLogin": "Пытаетесь войти в систему впервые? Учетные данные указаны в логах Frigate." } } diff --git a/web/public/locales/ru/components/camera.json b/web/public/locales/ru/components/camera.json index 3059b83f0..8a8c1a492 100644 --- a/web/public/locales/ru/components/camera.json +++ b/web/public/locales/ru/components/camera.json @@ -66,7 +66,8 @@ "title": "Настройки видеопотока {{cameraName}}", "stream": "Поток", "placeholder": "Выбрать поток" - } + }, + "birdseye": "Birdseye" } }, "debug": { diff --git a/web/public/locales/ru/components/dialog.json b/web/public/locales/ru/components/dialog.json index 078a37a97..b935670c2 100644 --- a/web/public/locales/ru/components/dialog.json +++ b/web/public/locales/ru/components/dialog.json @@ -65,12 +65,13 @@ "export": "Экспорт", "selectOrExport": "Выбрать или экспортировать", "toast": { - "success": "Экспорт успешно запущен. Файл доступен в папке /exports.", + "success": "Экспорт успешно запущен. Файл доступен на странице экспорта.", "error": { "failed": "Не удалось запустить экспорт: {{error}}", "noVaildTimeSelected": "Не выбран допустимый временной диапазон", "endTimeMustAfterStartTime": "Время окончания должно быть после времени начала" - } + }, + "view": "Просмотр" }, "fromTimeline": { "saveExport": "Сохранить экспорт", @@ -120,7 +121,16 @@ "button": { "export": "Экспорт", "markAsReviewed": "Пометить как просмотренное", - "deleteNow": "Удалить сейчас" + "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 index 024ebe02c..095ea91ba 100644 --- a/web/public/locales/ru/components/filter.json +++ b/web/public/locales/ru/components/filter.json @@ -116,12 +116,26 @@ "title": "Распознанные номерные знаки", "loadFailed": "Не удалось загрузить распознанные номерные знаки.", "loading": "Загрузка распознанных номерных знаков…", - "selectPlatesFromList": "Выберите один или более знаков из списка." + "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/views/classificationModel.json b/web/public/locales/ru/views/classificationModel.json new file mode 100644 index 000000000..b5b7e2222 --- /dev/null +++ b/web/public/locales/ru/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "documentTitle": "Классификация моделей - Frigate", + "details": { + "scoreInfo": "Оценка представляет собой среднюю степень достоверности классификации по всем обнаружениям данного объекта.", + "none": "Нет", + "unknown": "Неизвестно" + }, + "button": { + "deleteClassificationAttempts": "Удалить изображения классификации", + "renameCategory": "Переименовать класс", + "deleteCategory": "Удалить класс", + "deleteImages": "Удалить изображения", + "trainModel": "Тренировать модель", + "addClassification": "Добавить классификацию", + "deleteModels": "Удалить модели", + "editModel": "Редактировать модель" + }, + "toast": { + "success": { + "deletedCategory": "Класс удалён", + "deletedImage": "Изображения удалены", + "deletedModel_one": "Успешно удалена {{count}} модель", + "deletedModel_few": "Успешно удалены {{count}} модели", + "deletedModel_many": "Успешно удалены {{count}} моделей", + "categorizedImage": "Изображение успешно классифицировано", + "trainedModel": "Модель успешно обучена.", + "trainingModel": "Обучение модели успешно запущено.", + "updatedModel": "Конфигурация модели успешно обновлена", + "renamedCategory": "Класс успешно переименован в {{name}}" + }, + "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 index 73b566a08..0dd775b24 100644 --- a/web/public/locales/ru/views/configEditor.json +++ b/web/public/locales/ru/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Ошибка сохранения конфигурации" } }, - "confirm": "Выйти без сохранения?" + "confirm": "Выйти без сохранения?", + "safeConfigEditor": "Редактор конфигурации (безопасный режим)", + "safeModeDescription": "Frigate находится в безопасном режиме из-за ошибки проверки конфигурации." } diff --git a/web/public/locales/ru/views/events.json b/web/public/locales/ru/views/events.json index 6c8bebb6e..16fe307ca 100644 --- a/web/public/locales/ru/views/events.json +++ b/web/public/locales/ru/views/events.json @@ -35,5 +35,30 @@ "selected": "{{count}} выбрано", "selected_one": "{{count}} выбрано", "selected_other": "{{count}} выбрано", - "detected": "обнаружен" + "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 index 63f6c2867..3247544f5 100644 --- a/web/public/locales/ru/views/explore.json +++ b/web/public/locales/ru/views/explore.json @@ -48,12 +48,16 @@ "success": { "updatedSublabel": "Успешно обновлена дополнительная метка.", "updatedLPR": "Номерной знак успешно обновлён.", - "regenerate": "Новое описание запрошено у {{provider}}. В зависимости от скорости работы вашего провайдера, генерация нового описания может занять некоторое время." + "regenerate": "Новое описание запрошено у {{provider}}. В зависимости от скорости работы вашего провайдера, генерация нового описания может занять некоторое время.", + "audioTranscription": "Запрос на расшифровку аудио успешно отправлен. В зависимости от скорости вашего сервера Frigate, расшифровка может занять некоторое время.", + "updatedAttributes": "Атрибуты успешно обновлены." }, "error": { "updatedSublabelFailed": "Не удалось обновить дополнительную метку: {{errorMessage}}", "updatedLPRFailed": "Не удалось обновить номерной знак: {{errorMessage}}", - "regenerate": "Не удалось запросить новое описание у {{provider}}: {{errorMessage}}" + "regenerate": "Не удалось запросить новое описание у {{provider}}: {{errorMessage}}", + "audioTranscription": "Не удалось запросить транскрипцию аудио: {{errorMessage}}", + "updatedAttributesFailed": "Не удалось обновить атрибуты: {{errorMessage}}" } } }, @@ -98,14 +102,24 @@ "regenerateFromThumbnails": "Перегенерировать из миниатюры", "snapshotScore": { "label": "Оценка снимка" - } + }, + "score": { + "label": "Оценка" + }, + "editAttributes": { + "title": "Редактировать атрибуты", + "desc": "Выберите атрибуты классификации для этого {{label}}" + }, + "attributes": "Атрибуты классификации" }, "trackedObjectDetails": "Детали объекта", "type": { "details": "детали", "snapshot": "снимок", "video": "видео", - "object_lifecycle": "жизненный цикл объекта" + "object_lifecycle": "жизненный цикл объекта", + "thumbnail": "миниатюра", + "tracking_details": "подробности отслеживания" }, "objectLifecycle": { "title": "Жизненный цикл объекта", @@ -183,16 +197,38 @@ }, "deleteTrackedObject": { "label": "Удалить этот отслеживаемый объект" + }, + "addTrigger": { + "label": "Добавить триггер", + "aria": "Добавить триггер для этого отслеживаемого объекта" + }, + "audioTranscription": { + "label": "Транскрибировать", + "aria": "Запросить аудиотранскрипцию" + }, + "viewTrackingDetails": { + "label": "Просмотреть детали отслеживания", + "aria": "Показать детали отслеживания" + }, + "showObjectDetails": { + "label": "Показать путь объекта" + }, + "hideObjectDetails": { + "label": "Скрыть путь объекта" + }, + "downloadCleanSnapshot": { + "label": "Скачать чистый снимок", + "aria": "Скачать чистый снимок" } }, "dialog": { "confirmDelete": { "title": "Подтвердить удаление", - "desc": "Удаление этого отслеживаемого объекта приведёт к удалению его снимка, всех сохранённых эмбеддингов и записей жизненного цикла. Сами записи в разделе История НЕ будут удалены.

    Вы уверены, что хотите продолжить?" + "desc": "Удаление этого отслеживаемого объекта приведёт к удалению снимка, всех сохранённых эмбеддингов и всех связанных записей деталей отслеживания. Записанное видео этого отслеживаемого объекта в представлении Истории НЕ будет удалено.

    Вы уверены, что хотите продолжить?" } }, - "noTrackedObjects": "Не найдено отслеживаемых объектов", - "fetchingTrackedObjectsFailed": "При получении списка отслеживаемых объектов произошла ошибка: {{errorMessage}}", + "noTrackedObjects": "Отслеживаемые объекты не найдены", + "fetchingTrackedObjectsFailed": "Ошибка при получении отслеживаемых объектов: {{errorMessage}}", "trackedObjectsCount_one": "{{count}} отслеживаемый объект ", "trackedObjectsCount_few": "{{count}} отслеживаемых объекта ", "trackedObjectsCount_many": "{{count}} отслеживаемых объектов ", @@ -203,7 +239,64 @@ "error": "Не удалось удалить отслеживаемый объект: {{errorMessage}}" } }, - "tooltip": "Соответствие с {{type}} на {{confidence}}%" + "tooltip": "Соответствие с {{type}} на {{confidence}}%", + "previousTrackedObject": "Предыдущий отслеживаемый объект", + "nextTrackedObject": "Следующий отслеживаемый объект" }, - "exploreMore": "Просмотреть больше объектов {{label}}" + "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 index f48fb3e71..c14a578ca 100644 --- a/web/public/locales/ru/views/exports.json +++ b/web/public/locales/ru/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "Не удалось переименовать экспорт: {{errorMessage}}" } + }, + "tooltip": { + "shareExport": "Поделиться экспортом", + "downloadVideo": "Скачать видео", + "editName": "Изменить название", + "deleteExport": "Удалить экспорт" } } diff --git a/web/public/locales/ru/views/faceLibrary.json b/web/public/locales/ru/views/faceLibrary.json index 802f0cebe..90aa901d1 100644 --- a/web/public/locales/ru/views/faceLibrary.json +++ b/web/public/locales/ru/views/faceLibrary.json @@ -12,12 +12,12 @@ "documentTitle": "Библиотека лиц - Frigate", "description": { "placeholder": "Введите название коллекции", - "addFace": "Пошаговое добавление новой коллекции в Библиотеку лиц.", - "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчеркивания и дефисы." + "addFace": "Добавьте новую коллекцию в библиотеку лиц, загрузив свое первое изображение.", + "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы." }, "createFaceLibrary": { "desc": "Создание новой коллекции", - "nextSteps": "Для создания надежной базы:
  • Используйте вкладку Обучение, чтобы выбрать изображения и обучить систему для каждого обнаруженного человека.
  • Используйте фронтальные изображения для лучшего результата; избегайте изображений с лицами, снятыми под углом.
  • ", + "nextSteps": "Для создания надежной базы:
  • Используйте вкладку \"Недавние распознавания\", чтобы выбрать изображения каждого обнаруженного человека и обучить систему
  • Используйте фронтальные изображения для лучшего результата; избегайте изображений с лицами, снятыми под углом.
  • ", "title": "Создать коллекцию", "new": "Создать новое лицо" }, @@ -28,9 +28,10 @@ }, "selectItem": "Выбор {{item}}", "train": { - "aria": "Выбор обучения", - "title": "Обучение", - "empty": "Нет недавних попыток распознавания лиц" + "aria": "Выберите последние распознавания", + "title": "Последние распознавания", + "empty": "Нет недавних попыток распознавания лиц", + "titleShort": "Недавнее" }, "toast": { "success": { @@ -43,7 +44,7 @@ "uploadedImage": "Изображение успешно загружено.", "trainedFace": "Лицо успешно запомнено.", "addFaceLibrary": "{{name}} успешно добавлен(а) в Библиотеку лиц!", - "updatedFaceScore": "Оценка лица успешно обновлена.", + "updatedFaceScore": "Оценка лица успешно обновлена для {{name}} {{score}}.", "renamedFace": "Лицо успешно переименовано в {{name}}" }, "error": { @@ -62,7 +63,7 @@ }, "imageEntry": { "dropActive": "Перетащите изображение сюда…", - "dropInstructions": "Перетащите изображение сюда или нажмите для выбора", + "dropInstructions": "Перетащите или вставьте изображение сюда или щелкните, чтобы выбрать", "maxSize": "Макс. размер: {{size}}Мб", "validation": { "selectImage": "Пожалуйста, выберите файл изображения." diff --git a/web/public/locales/ru/views/live.json b/web/public/locales/ru/views/live.json index e7960d58f..9cda2d3c9 100644 --- a/web/public/locales/ru/views/live.json +++ b/web/public/locales/ru/views/live.json @@ -43,7 +43,15 @@ "label": "Кликните в кадре для центрирования PTZ-камеры" } }, - "presets": "Предустановки PTZ-камеры" + "presets": "Предустановки PTZ-камеры", + "focus": { + "in": { + "label": "Сфокусировать PTZ камеру на" + }, + "out": { + "label": "Отдалить фокус PTZ камеры" + } + } }, "camera": { "enable": "Включить камеру", @@ -78,8 +86,8 @@ "disable": "Скрыть статистику потока" }, "manualRecording": { - "title": "Запись по требованию", - "tips": "Создать ручное событие на основе настроек хранения записей этой камеры.", + "title": "По требованию", + "tips": "Скачать моментальный снимок или создать ручное событие, исходя из настроек хранения записей для этой камеры.", "playInBackground": { "label": "Воспроизведение в фоне", "desc": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." @@ -124,6 +132,9 @@ "playInBackground": { "label": "Воспроизвести в фоне", "tips": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "debug": { + "picker": "Выбор потока недоступен в режиме отладки. В отладочном представлении всегда используется поток, назначенный на роль обнаружения." } }, "cameraSettings": { @@ -133,7 +144,8 @@ "audioDetection": "Детекция аудио", "snapshots": "Снимки", "autotracking": "Автотрекинг", - "cameraEnabled": "Камера активирована" + "cameraEnabled": "Камера активирована", + "transcription": "Транскрипция аудио" }, "history": { "label": "Отобразить архивные записи" @@ -154,5 +166,24 @@ "exitEdit": "Выход из редактирования" }, "audio": "Аудио", - "notifications": "Уведомления" + "notifications": "Уведомления", + "transcription": { + "enable": "Включить транскрипцию звука в реальном времени", + "disable": "Выключить транскрипцию звука" + }, + "snapshot": { + "noVideoSource": "Нет видеоисточника для снимка.", + "captureFailed": "Не удалось сделать снимок.", + "takeSnapshot": "Скачать моментальный снимок", + "downloadStarted": "Загрузка снимка началась." + }, + "noCameras": { + "title": "Камеры не настроены", + "description": "Начните с подключения камеры к Frigate.", + "buttonText": "Добавить камеру", + "restricted": { + "title": "Нет доступных камер", + "description": "У вас нет разрешения на просмотр камер в этой группе." + } + } } diff --git a/web/public/locales/ru/views/search.json b/web/public/locales/ru/views/search.json index 0c7f8477f..cf90fb152 100644 --- a/web/public/locales/ru/views/search.json +++ b/web/public/locales/ru/views/search.json @@ -26,7 +26,8 @@ "max_speed": "Макс. скорость", "has_clip": "Есть клип", "has_snapshot": "Есть снимок", - "labels": "Метки" + "labels": "Метки", + "attributes": "Атрибуты" }, "searchType": { "thumbnail": "Миниатюра", diff --git a/web/public/locales/ru/views/settings.json b/web/public/locales/ru/views/settings.json index 130b56619..504c51178 100644 --- a/web/public/locales/ru/views/settings.json +++ b/web/public/locales/ru/views/settings.json @@ -4,13 +4,15 @@ "camera": "Настройки камеры - Frigate", "masksAndZones": "Маски и Зоны - Frigate", "motionTuner": "Детекции движения - Frigate", - "general": "Общие настройки - Frigate", + "general": "Настройки интерфейса - Frigate", "frigatePlus": "Настройки Frigate+ - Frigate", "authentication": "Настройки аутентификации - Frigate", "classification": "Настройки распознавания - Frigate", "object": "Отладка - Frigate", "notifications": "Настройки уведомлений - Frigate", - "enrichments": "Настройки обогащения - Frigate" + "enrichments": "Настройки обогащения - Frigate", + "cameraManagement": "Управление камерами - Frigate", + "cameraReview": "Настройки просмотра камеры - Frigate" }, "menu": { "cameras": "Настройки камеры", @@ -22,7 +24,11 @@ "frigateplus": "Frigate+", "ui": "Интерфейс", "classification": "Распознавание", - "enrichments": "Обогащения" + "enrichments": "Обогащения", + "triggers": "Триггеры", + "cameraManagement": "Управление", + "cameraReview": "Обзор", + "roles": "Роли" }, "dialog": { "unsavedChanges": { @@ -35,7 +41,7 @@ "noCamera": "Нет камеры" }, "general": { - "title": "Общие настройки", + "title": "Настройки интерфейса", "liveDashboard": { "title": "Панель мониторинга", "automaticLiveView": { @@ -45,6 +51,14 @@ "playAlertVideos": { "label": "Воспроизводить видео с тревогами", "desc": "По умолчанию последние тревоги на панели мониторинга воспроизводятся как короткие зацикленные видео. Отключите эту опцию, чтобы показывать только статичное изображение последних оповещений на этом устройстве/браузере." + }, + "displayCameraNames": { + "label": "Всегда показывать названия камер", + "desc": "Всегда показывать названия камер в виде метки на панели мониторинга с несколькими камерами." + }, + "liveFallbackTimeout": { + "label": "Таймаут переключения на низкое качество", + "desc": "Когда высококачественный поток камеры недоступен, переключиться на режим низкой пропускной способности через указанное количество секунд. По умолчанию: 3." } }, "calendar": { @@ -153,7 +167,12 @@ "setPassword": "Установить пароль", "desc": "Создайте надежный пароль для защиты аккаунта.", "cannotBeEmpty": "Пароль не может быть пустым", - "doNotMatch": "Пароли не совпадают" + "doNotMatch": "Пароли не совпадают", + "currentPasswordRequired": "Текущий пароль обязателен", + "incorrectCurrentPassword": "Текущий пароль указан неверно", + "passwordVerificationFailed": "Не удалось проверить пароль", + "multiDeviceWarning": "Все остальные устройства, на которых вы вошли в систему, потребуют повторного входа в течение {{refresh_time}}.", + "multiDeviceAdmin": "Вы также можете принудительно заставить всех пользователей повторно пройти аутентификацию немедленно, обновив свой JWT-секрет." }, "deleteUser": { "warn": "Вы уверены, что хотите удалить пользователя {{username}}?", @@ -168,7 +187,8 @@ "viewer": "Наблюдатель", "viewerDesc": "Доступны только панель мониторинга, обзор событий, поиск и экспорт данных.", "admin": "Администратор", - "adminDesc": "Полный доступ ко всем функциям." + "adminDesc": "Полный доступ ко всем функциям.", + "customDesc": "Роль с настраиваемыми правами доступа к определённым камерам." }, "select": "Выбрать роль" }, @@ -193,7 +213,16 @@ "veryStrong": "Очень сложный" }, "match": "Пароли совпадают", - "notMatch": "Пароли не совпадают" + "notMatch": "Пароли не совпадают", + "show": "Показать пароль", + "hide": "Скрыть пароль", + "requirements": { + "title": "Требования к паролю:", + "length": "Не менее 8 символов", + "uppercase": "Как минимум одна заглавная буква", + "digit": "Как минимум одна цифра", + "special": "Хотя бы один специальный символ (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "Новый пароль", @@ -203,7 +232,11 @@ "placeholder": "Введите новый пароль" }, "usernameIsRequired": "Необходимо ввести имя пользователя", - "passwordIsRequired": "Требуется пароль" + "passwordIsRequired": "Требуется пароль", + "currentPassword": { + "title": "Текущий пароль", + "placeholder": "Введите ваш текущий пароль" + } }, "createUser": { "title": "Создать нового пользователя", @@ -230,7 +263,7 @@ "table": { "username": "Имя пользователя", "actions": "Действия", - "password": "Пароль", + "password": "Сбросить пароль", "noUsers": "Пользователей не найдено.", "changeRole": "Изменить роль пользователя", "role": "Роль", @@ -240,7 +273,7 @@ "title": "Управление пользователями", "desc": "Управление учетными записями пользователей Frigate." }, - "updatePassword": "Обновить пароль", + "updatePassword": "Сбросить пароль", "addUser": "Добавить пользователя" }, "notification": { @@ -330,6 +363,44 @@ "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": { @@ -362,7 +433,7 @@ "name": { "title": "Название", "inputPlaceHolder": "Введите название…", - "tips": "Название должно содержать не менее 2 символов и не совпадать с названием камеры или другой зоны." + "tips": "Имя должно содержать не менее 2 символов, включать хотя бы одну букву и не должно совпадать с названием камеры или другой зоны на этой камере." }, "inertia": { "title": "Инерция", @@ -384,7 +455,7 @@ "desc": "Задаёт минимальную скорость объектов для учёта в этой зоне." }, "toast": { - "success": "Зона ({{zoneName}}) сохранена. Перезапустите Frigate для применения изменений." + "success": "Зона ({{zoneName}}) сохранена." } }, "motionMasks": { @@ -411,8 +482,8 @@ "documentTitle": "Редактирование маски движения - Frigate", "toast": { "success": { - "title": "{{polygonName}} сохранена. Перезапустите Frigate для применения изменений.", - "noName": "Маска движения сохранена. Перезапустите Frigate для применения изменений." + "title": "{{polygonName}} сохранена.", + "noName": "Маска движения сохранена." } } }, @@ -426,7 +497,8 @@ "mustNotBeSameWithCamera": "Имя зоны не должно совпадать с именем камеры.", "hasIllegalCharacter": "Имя зоны содержит недопустимые символы.", "alreadyExists": "Зона с таким именем уже существует для этой камеры.", - "mustNotContainPeriod": "Имя зоны не должно содержать точки." + "mustNotContainPeriod": "Имя зоны не должно содержать точки.", + "mustHaveAtLeastOneLetter": "Название зоны должно содержать хотя бы одну букву." } }, "distance": { @@ -498,8 +570,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} сохранена. Перезапустите Frigate для применения изменений.", - "noName": "Маска объектов сохранена. Перезапустите Frigate для применения изменений." + "title": "{{polygonName}} сохранена.", + "noName": "Маска объектов сохранена." } } }, @@ -575,6 +647,19 @@ "title": "Регионы", "desc": "Показать рамку области интереса, отправленной детектору объектов", "tips": "

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


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

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

    Пути


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

    " + }, + "openCameraWebUI": "Открыть веб-интерфейс {{camera}}", + "audio": { + "title": "Аудио", + "noAudioDetections": "Аудиообнаружений нет", + "score": "оценка", + "currentRMS": "Текущий RMS", + "currentdbFS": "Текущий dbFS" } }, "frigatePlus": { @@ -683,5 +768,495 @@ "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." + }, + "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}} успешно сохранена" + } + } + }, + "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 для применения изменений." + } + } } } diff --git a/web/public/locales/ru/views/system.json b/web/public/locales/ru/views/system.json index 3e0052a88..f3f7a3d95 100644 --- a/web/public/locales/ru/views/system.json +++ b/web/public/locales/ru/views/system.json @@ -42,7 +42,8 @@ "inferenceSpeed": "Скорость вывода детектора", "cpuUsage": "Использование CPU детектором", "memoryUsage": "Использование памяти детектором", - "temperature": "Температура детектора" + "temperature": "Температура детектора", + "cpuUsageInformation": "CPU используется при подготовке входных и выходных данных к/от моделей обнаружения. Это значение не измеряет использование вывода, даже если использовать GPU или ускоритель." }, "hardwareInfo": { "title": "Информация об оборудовании", @@ -75,7 +76,12 @@ } }, "npuMemory": "Память NPU", - "npuUsage": "Использование NPU" + "npuUsage": "Использование NPU", + "intelGpuWarning": { + "title": "Предупреждение: статистика Intel GPU", + "message": "Статистика GPU недоступна", + "description": "Это известная ошибка в инструментах отчетности статистики Intel GPU (intel_gpu_top), из-за которой они ломаются и постоянно возвращают уровень использования GPU 0%, даже в случаях, когда аппаратное ускорение и обнаружение объектов корректно работают на (i)GPU. Это не ошибка Frigate. Вы можете перезапустить хост-систему, чтобы временно устранить проблему и убедиться, что GPU работает правильно. На производительность это не влияет." + } }, "otherProcesses": { "title": "Другие процессы", @@ -102,6 +108,10 @@ "title": "Не используется", "tips": "Это значение может неточно отражать свободное место, доступное Frigate, если на вашем диске есть другие файлы помимо записей Frigate. Frigate не отслеживает использование хранилища за пределами своих записей." } + }, + "shm": { + "title": "Выделение разделяемой памяти", + "warning": "Текущеее значение разделяемой памяти в {{total}}MB слишком мало. Увеличьте его хотя бы до {{min_shm}}MB." } }, "cameras": { @@ -158,7 +168,8 @@ "reindexingEmbeddings": "Переиндексация эмбеддингов (выполнено {{processed}} %)", "cameraIsOffline": "{{camera}} отключена", "detectIsVerySlow": "{{detect}} идёт очень медленно ({{speed}} мс)", - "detectIsSlow": "{{detect}} идёт медленно ({{speed}} мс)" + "detectIsSlow": "{{detect}} идёт медленно ({{speed}} мс)", + "shmTooLow": "Объем выделенной памяти /dev/shm ({{total}} МБ) должен быть увеличен как минимум до {{min}} МБ." }, "enrichments": { "title": "Обогащение данных", @@ -174,7 +185,17 @@ "yolov9_plate_detection": "Обнаружение номеров YOLOv9", "face_recognition": "Распознавание лиц", "plate_recognition": "Распознавание номеров", - "image_embedding": "Векторизация изображений" - } + "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 index 8a10ee24a..56129353f 100644 --- a/web/public/locales/sk/audio.json +++ b/web/public/locales/sk/audio.json @@ -2,7 +2,7 @@ "speech": "Reč", "babbling": "Bľabotanie", "yell": "Krik", - "bellow": "Rev", + "bellow": "Pod", "whispering": "Šepkanie", "whoop": "Výskanie", "laughter": "Smiech", @@ -47,5 +47,457 @@ "horse": "Kôň", "sheep": "Ovce", "camera": "Kamera", - "pant": "Oddychávanie" + "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 index 28812e247..199493fdd 100644 --- a/web/public/locales/sk/common.json +++ b/web/public/locales/sk/common.json @@ -39,7 +39,266 @@ "hour_few": "{{time}}hodiny", "hour_other": "{{time}}hodin", "m": "{{time}} min", - "s": "{{time}}s" + "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" }, - "selectItem": "Vyberte {{item}}" + "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/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "readTheDocumentation": "Prečítajte si dokumentáciu", + "label": { + "back": "Choď späť", + "hide": "Skryť {{item}}", + "show": "Zobraziť {{item}}", + "ID": "ID", + "none": "None", + "all": "Všetko" + }, + "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ť" + }, + "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" + } + }, + "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" + }, + "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" + } + } + }, + "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" + } } diff --git a/web/public/locales/sk/components/auth.json b/web/public/locales/sk/components/auth.json index a59f7d0a5..5d44c93c7 100644 --- a/web/public/locales/sk/components/auth.json +++ b/web/public/locales/sk/components/auth.json @@ -10,6 +10,7 @@ "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 index 151199d9a..e2245bd07 100644 --- a/web/public/locales/sk/components/camera.json +++ b/web/public/locales/sk/components/camera.json @@ -56,12 +56,32 @@ "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." + "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" } + }, + "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 index a254150e2..6904bc0d2 100644 --- a/web/public/locales/sk/components/dialog.json +++ b/web/public/locales/sk/components/dialog.json @@ -41,7 +41,10 @@ "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" @@ -50,12 +53,13 @@ "export": "Exportovať", "selectOrExport": "Vybrať pre Export", "toast": { - "success": "Export úspešne spustený. Súbor nájdete v adresári /exports.", + "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", @@ -67,8 +71,54 @@ "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." + "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 index e1c1eb472..83305f921 100644 --- a/web/public/locales/sk/components/filter.json +++ b/web/public/locales/sk/components/filter.json @@ -54,6 +54,83 @@ "relevance": "Relevantnosť" }, "cameras": { - "label": "Filter kamier" + "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é SPZ.", + "selectPlatesFromList": "Vyberte jeden alebo viacero tanierov zo zoznamu.", + "selectAll": "Vybrať všetko", + "clearAll": "Vymazať všetko" } } diff --git a/web/public/locales/sk/objects.json b/web/public/locales/sk/objects.json index 2b3199df7..42ec664e2 100644 --- a/web/public/locales/sk/objects.json +++ b/web/public/locales/sk/objects.json @@ -31,5 +31,90 @@ "handbag": "Kabelka", "tie": "Kravata", "suitcase": "Kufor", - "frisbee": "Frisbee" + "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": "ŠPZ", + "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/classificationModel.json b/web/public/locales/sk/views/classificationModel.json new file mode 100644 index 000000000..f8529ea20 --- /dev/null +++ b/web/public/locales/sk/views/classificationModel.json @@ -0,0 +1,182 @@ +{ + "documentTitle": "Klasifikačné modely", + "button": { + "deleteClassificationAttempts": "Odstrániť obrázky klasifikácie", + "renameCategory": "Premenovať triedu", + "deleteCategory": "Odstrániť triedu", + "deleteImages": "Odstrániť obrázky", + "trainModel": "Model vlaku", + "addClassification": "Pridať klasifikáciu", + "deleteModels": "Odstrániť modely", + "editModel": "Editovať model" + }, + "toast": { + "success": { + "deletedCategory": "Vymazaná trieda", + "deletedImage": "Vymazané obrázky", + "categorizedImage": "Obrázok bol úspešne klasifikovaný", + "trainedModel": "Úspešne vyškolený model.", + "trainingModel": "Úspešne spustený modelový tréning.", + "deletedModel_one": "Úspešne zmazané {{count}} model (y)", + "deletedModel_few": "", + "deletedModel_other": "", + "updatedModel": "Úspešne zmenená konfigurácia modelu", + "renamedCategory": "Úspešne premenovaná trieda na" + }, + "error": { + "deleteImageFailed": "Nepodarilo sa odstrániť: {{errorMessage}}", + "deleteCategoryFailed": "Nepodarilo sa odstrániť triedu: {{errorMessage}}", + "categorizeFailed": "Nepodarilo sa kategorizovať obrázok: {{errorMessage}}", + "trainingFailed": "Nepodarilo sa spustiť trénovanie modelu: {{errorMessage}}", + "deleteModelFailed": "Nepodarilo sa odstrániť model: {{errorMessage}}", + "trainingFailedToStart": "Neuspešny štart trenovania modelu:", + "updateModelFailed": "Chyba pri úprave modelu:", + "renameCategoryFailed": "Chyba pri premenovani triedy:" + } + }, + "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": "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ť obrázky vlakov", + "desc": "Naozaj chcete odstrániť {{count}} obrázkov? Túto akciu nie je možné vrátiť späť." + }, + "renameCategory": { + "title": "Premenovať triedu", + "desc": "Zadajte nový názov 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" + }, + "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" + } + } + }, + "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": "Ste si istí, že chcete odstrániť {{count}} model (y)? 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á." + }, + "menu": { + "objects": "Objekty", + "states": "Štátov" + }, + "details": { + "scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu." + }, + "tooltip": { + "trainingInProgress": "Model sa aktuálne trénuje", + "noNewImages": "Žiadne nové obrázky na trénovanie. Najskor 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 modelu", + "descriptionState": "Upravte triedy pre tento model klasifikácie. Zmeny budú vyžadovať pretrénovanie modelu.", + "descriptionObject": "Upravte typ objektu a typ klasifikácie pre tento 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 index 7bfafd009..c10f789a8 100644 --- a/web/public/locales/sk/views/configEditor.json +++ b/web/public/locales/sk/views/configEditor.json @@ -12,5 +12,7 @@ "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 index 32d23889d..fe86d41d5 100644 --- a/web/public/locales/sk/views/events.json +++ b/web/public/locales/sk/views/events.json @@ -34,5 +34,29 @@ "selected_one": "{{count}} vybraných", "selected_other": "{{count}} vybraných", "camera": "Kamera", - "detected": "Detekované" + "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 index 5de31b69f..223eb80fd 100644 --- a/web/public/locales/sk/views/explore.json +++ b/web/public/locales/sk/views/explore.json @@ -2,7 +2,80 @@ "documentTitle": "Preskúmať - Frigate", "generativeAI": "Generatívna AI", "details": { - "timestamp": "Časová pečiatka" + "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": { @@ -38,7 +111,9 @@ "details": "detaily", "snapshot": "snímka", "video": "video", - "object_lifecycle": "životný cyklus objektu" + "object_lifecycle": "životný cyklus objektu", + "thumbnail": "Náhľad", + "tracking_details": "Pohybové detaili" }, "objectLifecycle": { "title": "Životný cyklus Objektu", @@ -48,6 +123,173 @@ "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" + "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. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "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 index 53c83f090..d9df68500 100644 --- a/web/public/locales/sk/views/exports.json +++ b/web/public/locales/sk/views/exports.json @@ -13,5 +13,11 @@ "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 index 81d546142..ba46fda1f 100644 --- a/web/public/locales/sk/views/faceLibrary.json +++ b/web/public/locales/sk/views/faceLibrary.json @@ -1,7 +1,7 @@ { "description": { - "addFace": "Sprievodca pridáním novej kolekcie do Knižnice tvárí.", - "invalidName": "Neplatný názov. Názov može obsahovať iba písmená, čísla, medzery, apostrofy, podtržníky a pomlčky.", + "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": { @@ -23,7 +23,7 @@ "title": "Vytvoriť Zbierku", "desc": "Vytvoriť novú zbierku", "new": "Vytvoriť novú tvár", - "nextSteps": "Vybudovanie pevných základov:
  • Pomocou záložky Tréning vyberte a trénujte obrázky pre každú detekovanú osobu.
  • Pre dosiahnutie najlepších výsledkov sa zamerajte na snímky s priamym pohľadom; vyhnite sa snímkam, ktoré zachytávajú tváre pod uhlom.
  • " + "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", @@ -34,8 +34,8 @@ } }, "train": { - "title": "Trénovať", - "aria": "Vybrať tréning", + "title": "Nedávne uznania", + "aria": "Vyberte posledné rozpoznania", "empty": "Neexistujú žiadne predchádzajúce pokusy o rozpoznávanie tváre" }, "selectItem": "Vyberte {{item}}", @@ -67,7 +67,7 @@ "selectImage": "Vyberte súbor s obrázkom." }, "dropActive": "Presunte obrázok sem…", - "dropInstructions": "Potiahnite sem obrázok alebo ho vyberte kliknutím", + "dropInstructions": "Pretiahnite obrázok tu, alebo kliknite na výber", "maxSize": "Max velkosť: {{size}} MB" }, "nofaces": "Žiadne tváre", diff --git a/web/public/locales/sk/views/live.json b/web/public/locales/sk/views/live.json index ebb12d4cd..546a6035e 100644 --- a/web/public/locales/sk/views/live.json +++ b/web/public/locales/sk/views/live.json @@ -43,10 +43,18 @@ "label": "Kliknite do rámčeka pre vycentrovanie PTZ kamery" } }, - "presets": "Predvoľby PTZ kamery" + "presets": "Predvoľby PTZ kamery", + "focus": { + "in": { + "label": "Zaostrenie PTZ kamery v" + }, + "out": { + "label": "Výstup zaostrenia PTZ kamery" + } + } }, "camera": { - "enable": "Povoliť fotoaparát", + "enable": "Povoliť kameru", "disable": "Zakázať kameru" }, "muteCameras": { @@ -72,5 +80,108 @@ "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/search.json b/web/public/locales/sk/views/search.json index cc567af26..a368ca123 100644 --- a/web/public/locales/sk/views/search.json +++ b/web/public/locales/sk/views/search.json @@ -41,6 +41,32 @@ "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 index 27013c197..900236606 100644 --- a/web/public/locales/sk/views/settings.json +++ b/web/public/locales/sk/views/settings.json @@ -2,14 +2,16 @@ "documentTitle": { "default": "Nastavenia - Frigate", "authentication": "Nastavenie autentifikácie- Frigate", - "camera": "Nastavenia fotoaparátu – 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": "Všeobecné nastavenia – Frigate", + "general": "UI nastavenia – Frigate", "frigatePlus": "Nastavenia Frigate+ – Frigate", - "notifications": "Nastavenia upozornení – Frigate" + "notifications": "Nastavenia upozornení – Frigate", + "cameraManagement": "Manažment kamier - Frigate", + "cameraReview": "Nastavenie kamier - Frigate" }, "menu": { "ui": "Uživaťelské rozohranie", @@ -20,7 +22,11 @@ "debug": "Ladenie", "users": "Uživatelia", "notifications": "Notifikacie", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "triggers": "Spúšťače", + "roles": "Roly", + "cameraManagement": "Manažment", + "cameraReview": "Recenzia" }, "dialog": { "unsavedChanges": { @@ -33,7 +39,7 @@ "noCamera": "Žiadna Kamera" }, "general": { - "title": "Hlavné nastavenia", + "title": "UI nastavenia", "liveDashboard": { "title": "Živý Dashboard", "automaticLiveView": { @@ -43,12 +49,1162 @@ "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 ŠPZ", + "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." + }, + "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á. Reštartujte Frigate pre aplikovanie zmien." + }, + "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ý. Reštartujte Frigate pre aplikovanie zmien.", + "noName": "Maska Detekcia pohybu bola uložená. Reštartujte Frigate pre aplikovanie zmien." + } + } + }, + "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ý. Reštartujte Frigate pre aplikovanie zmien.", + "noName": "Maska Objektu bola uložená. Reštartujte Frigate pre aplikovanie zmien." + } + } + }, + "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 umožňujú/disable Generovať opisy objektu AI pre tento fotoaparát. Keď je zakázané, AI vygenerované popisy nebudú požiadané o sledovanie objektov na tomto fotoaparáte." + }, + "review_descriptions": { + "title": "Popisy generatívnej umelej inteligencie", + "desc": "Dočasne povoliť/disable Genive AI opisy pre tento fotoaparát. Keď je zakázané, AI vygenerované popisy nebudú požiadané o preskúmanie položiek na tomto fotoaparáte." + }, + "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": "Aktualizovať 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": "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": "", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "{{count}} užívatelia priradené tejto úlohe boli aktualizované pre \"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 index ea3a3927e..94afc9111 100644 --- a/web/public/locales/sk/views/system.json +++ b/web/public/locales/sk/views/system.json @@ -42,7 +42,8 @@ "inferenceSpeed": "Detekčná rýchlosť", "temperature": "Detekčná teplota", "cpuUsage": "Detektor využitia CPU", - "memoryUsage": "Detektor využitia pamäte" + "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", @@ -52,9 +53,146 @@ "gpuDecoder": "GPU dekodér", "gpuInfo": { "vainfoOutput": { - "title": "Výstup Vainfo" + "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" + } + }, + "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}}snimky 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 ŠPZ", + "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 ŠPZ", + "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" + }, + "averageInf": "Priemerný čas inferencie" } } diff --git a/web/public/locales/sl/audio.json b/web/public/locales/sl/audio.json index 31562e8c9..bf5482cab 100644 --- a/web/public/locales/sl/audio.json +++ b/web/public/locales/sl/audio.json @@ -106,5 +106,39 @@ "piano": "Klavir", "electric_piano": "Digitalni klavir", "organ": "Orgle", - "electronic_organ": "Digitalne orgle" + "electronic_organ": "Digitalne orgle", + "chant": "Spev", + "mantra": "Mantra", + "child_singing": "Otroško petje", + "synthetic_singing": "Sintetično petje", + "humming": "Brenčanje", + "groan": "Stok", + "grunt": "Godrnjanje", + "wheeze": "Zadihan izdih", + "gasp": "Glasen Vzdih", + "pant": "Sopihanje", + "snort": "Smrkanje", + "throat_clearing": "Odkašljevanje", + "sneeze": "Kihanje", + "sniff": "Vohljaj", + "chewing": "Žvečenje", + "biting": "Grizenje", + "gargling": "Grgranje", + "stomach_rumble": "Grmotanje v Želodcu", + "heart_murmur": "Šum na Srcu", + "chatter": "Klepetanje", + "yip": "Jip", + "growling": "Rjovenje", + "whimper_dog": "Pasje Cviljenje", + "oink": "Oink", + "gobble": "Zvok Purana", + "wild_animals": "Divje Živali", + "roaring_cats": "Rjoveče Mačke", + "roar": "Rjovenje Živali", + "squawk": "Krik", + "patter": "Klepetanje", + "croak": "Kvakanje", + "rattle": "Ropotanje", + "whale_vocalization": "Kitova Vokalizacija", + "plucked_string_instrument": "Trgani Godalni Instrument" } diff --git a/web/public/locales/sl/common.json b/web/public/locales/sl/common.json index ff21c10ce..25bc0644e 100644 --- a/web/public/locales/sl/common.json +++ b/web/public/locales/sl/common.json @@ -51,7 +51,43 @@ "h": "{{time}}h", "m": "{{time}}m", "s": "{{time}}s", - "yr": "le" + "yr": "{{time}}l.", + "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" + }, + "invalidStartTime": "Napačen čas začetka", + "invalidEndTime": "Napačen čas konca", + "inProgress": "V teku" }, "menu": { "live": { @@ -67,9 +103,94 @@ }, "explore": "Brskanje", "theme": { - "nord": "Nord" + "nord": "Nord", + "label": "Teme", + "blue": "Modra", + "green": "Zelena", + "red": "Rdeča", + "highcontrast": "Visok Kontrast", + "default": "Privzeto" }, - "review": "Pregled" + "review": "Pregled", + "system": "Sistem", + "systemMetrics": "Sistemske metrike", + "configuration": "Konfiguracija", + "systemLogs": "Sistemski dnevniki", + "settings": "Nastavitve", + "configurationEditor": "Urejevalnik Konfiguracije", + "languages": "Jeziki", + "language": { + "en": "English (angleščina)", + "es": "Español (španščina)", + "zhCN": "简体中文 (poenostavljena kitajščina)", + "hi": "हिन्दी (hindijščina)", + "fr": "Français (francoščina)", + "ar": "العربية (arabščina)", + "pt": "Português (portugalščina)", + "ru": "Русский (ruščina)", + "de": "Deutsch (nemščina)", + "ja": "日本語 (japonščina)", + "tr": "Türkçe (turščina)", + "it": "Italiano (italijanščina)", + "nl": "Nederlands (nizozemščina)", + "sv": "Svenska (švedščina)", + "cs": "Čeština (češčina)", + "nb": "Norsk Bokmål (norveščina, bokmal)", + "ko": "한국어 (korejščina)", + "vi": "Tiếng Việt (vietnamščina)", + "fa": "فارسی (perzijščina)", + "pl": "Polski (poljščina)", + "uk": "Українська (ukrajinščina)", + "he": "עברית (hebrejščina)", + "el": "Ελληνικά (grščina)", + "ro": "Română (romunščina)", + "hu": "Magyar (madžarščina)", + "fi": "Suomi (finščina)", + "da": "Dansk (danščina)", + "sk": "Slovenčina (slovaščina)", + "yue": "粵語 (kantonščina)", + "th": "ไทย (tajščina)", + "sr": "Српски (srbščina)", + "sl": "Slovenščina (Slovenščina )", + "bg": "Български (bulgarščina)", + "withSystem": { + "label": "Uporabi sistemske nastavitve za jezik" + }, + "ptBR": "Português brasileiro (Brazilska portugalščina)", + "ca": "Català (Katalonščina)", + "lt": "Lietuvių (Litovščina)", + "gl": "Galego (Galicijščina)", + "id": "Bahasa Indonesia (Indonezijščina)", + "ur": "اردو (Urdujščina)" + }, + "appearance": "Izgled", + "darkMode": { + "label": "Temni Način", + "light": "Svetlo", + "dark": "Temno", + "withSystem": { + "label": "Uporabi sistemske nastavitve za svetel ali temen način" + } + }, + "withSystem": "Sistem", + "help": "Pomoč", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Znova Zaženi Frigate", + "export": "Izvoz", + "faceLibrary": "Zbirka Obrazov", + "user": { + "title": "Uporabnik", + "account": "Račun", + "current": "Trenutni Uporabnik: {{user}}", + "anonymous": "anonimen", + "logout": "Odjava", + "setPassword": "Nastavi Geslo" + }, + "uiPlayground": "UI Peskovnik", + "classification": "Klasifikacija" }, "button": { "apply": "Uporabi", @@ -80,7 +201,7 @@ "back": "Nazaj", "pictureInPicture": "Slika v Sliki", "history": "Zgodovina", - "disabled": "Izklopljeno", + "disabled": "Onemogočeno", "copy": "Kopiraj", "exitFullscreen": "Izhod iz Celozaslonskega načina", "enabled": "Omogočen", @@ -88,7 +209,26 @@ "save": "Shrani", "saving": "Shranjevanje …", "cancel": "Prekliči", - "fullscreen": "Celozaslonski način" + "fullscreen": "Celozaslonski način", + "twoWayTalk": "Dvosmerni Pogovor", + "cameraAudio": "Zvok Kamere", + "on": "Vključen", + "off": "Izključen", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Izbriši", + "yes": "Da", + "no": "Ne", + "download": "Prenesi", + "info": "Info", + "suspended": "Začasno ustavljeno", + "unsuspended": "Obnovi", + "play": "Predvajaj", + "unselect": "Odznači", + "export": "Izvoz", + "deleteNow": "Izbriši Zdaj", + "next": "Naprej", + "continue": "Nadaljuj" }, "unit": { "speed": { @@ -98,14 +238,74 @@ "length": { "feet": "čevelj", "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/uro", + "mbph": "MB/uro", + "gbph": "GB/uro" } }, "label": { - "back": "Pojdi nazaj" + "back": "Pojdi nazaj", + "hide": "Skrij {{item}}", + "show": "Prikaži {{item}}", + "ID": "ID", + "none": "Brez", + "all": "Vse" }, "pagination": { "next": { - "label": "Pojdi na naslednjo stran" + "label": "Pojdi na naslednjo stran", + "title": "Naprej" + }, + "label": "paginacija", + "previous": { + "title": "Prejšnji", + "label": "Pojdi na prejšnjo stran" + }, + "more": "Več strani" + }, + "selectItem": "Izberi {{item}}", + "toast": { + "copyUrlToClipboard": "Povezava kopirana v odložišče.", + "save": { + "title": "Shrani", + "error": { + "title": "Napaka pri shranjevanju sprememb: {{errorMessage}}", + "noMessage": "Napaka pri shranjevanju sprememb konfiguracije" + } } + }, + "role": { + "title": "Vloga", + "admin": "Administrator", + "viewer": "Gledalec", + "desc": "Administratorji imajo poln dostop do vseh funkcij Frigate uporabniškega vmesnika. Gledalci so omejeni na gledanje kamer, zgodovine posnetkov in pregledovanje dogodkov." + }, + "accessDenied": { + "documentTitle": "Dostop zavrnjen - Frigate", + "title": "Dostop Zavrnjen", + "desc": "Nimate pravic za ogled te strani." + }, + "notFound": { + "documentTitle": "Ni Najdeno - Frigate", + "title": "404", + "desc": "Stran ni najdena" + }, + "readTheDocumentation": "Preberite dokumentacijo", + "list": { + "two": "{{0}} in {{1}}", + "many": "{{items}}, in {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Izbirno", + "internalID": "Interni ID, ki ga Frigate uporablja v konfiguraciji in podatkovni bazi" + }, + "information": { + "pixels": "{{area}}px" } } diff --git a/web/public/locales/sl/components/camera.json b/web/public/locales/sl/components/camera.json index 9ee8f4046..10414fed1 100644 --- a/web/public/locales/sl/components/camera.json +++ b/web/public/locales/sl/components/camera.json @@ -50,7 +50,8 @@ }, "placeholder": "Izberite tok", "stream": "Tok" - } + }, + "birdseye": "Ptičji pogled" }, "name": { "label": "Ime", diff --git a/web/public/locales/sl/components/dialog.json b/web/public/locales/sl/components/dialog.json index e63f7c34b..f0284ee0f 100644 --- a/web/public/locales/sl/components/dialog.json +++ b/web/public/locales/sl/components/dialog.json @@ -12,11 +12,18 @@ "plus": { "review": { "question": { - "ask_full": "Ali je ta objekt {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Ali je ta objekt {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potrdi to oznako za Frigate Plus", + "ask_a": "Ali je ta objekt {{label}}?", + "ask_an": "Ali je ta objekt {{label}}?" }, "state": { "submitted": "Oddano" } + }, + "submitToPlus": { + "label": "Pošlji v Frigate+", + "desc": "Predmeti na lokacijah, ki se jim želite izogniti, niso lažni alarmi. Če jih označite kot lažne alarme, boste zmedli model." } }, "video": { @@ -25,10 +32,92 @@ }, "export": { "time": { - "lastHour_one": "Zadnja ura", + "lastHour_one": "Zadnja {{count}} ura", "lastHour_two": "Zadnji {{count}} uri", "lastHour_few": "Zadnje {{count}} ure", - "lastHour_other": "Zadnjih {{count}} ur" + "lastHour_other": "Zadnjih {{count}} ur", + "fromTimeline": "Izberi s Časovnice", + "custom": "Po meri", + "start": { + "title": "Začetni čas", + "label": "Izberi Začetni Čas" + }, + "end": { + "title": "Končni Čas", + "label": "Izberi Končni Čas" + } + }, + "name": { + "placeholder": "Poimenujte Izvoz" + }, + "select": "Izberi", + "export": "Izvoz", + "selectOrExport": "Izberi ali Izvozi", + "toast": { + "success": "Izvoz se je uspešno začel. Datoteko si oglejte v izvozih.", + "error": { + "failed": "Npaka pri začetku izvoza: {{error}}", + "endTimeMustAfterStartTime": "Končni čas mora biti po začetnem čase", + "noVaildTimeSelected": "Ni izbranega veljavnega časovnega obdobja" + } + }, + "fromTimeline": { + "saveExport": "Shrani Izvoz", + "previewExport": "Predogled Izvoza" } + }, + "streaming": { + "label": "Pretakanje", + "restreaming": { + "disabled": "Ponovno pretakanje za to kamero ni omogočeno.", + "desc": { + "title": "Za dodatne možnosti ogleda v živo in zvoka za to kamero nastavite go2rtc.", + "readTheDocumentation": "Preberi dokumentacijo" + } + }, + "showStats": { + "label": "Prikaži statistiko pretoka", + "desc": "Omogočite to možnost, če želite prikazati statistiko pretoka videa kamere." + }, + "debugView": "Pogled za Odpravljanje Napak" + }, + "search": { + "saveSearch": { + "label": "Varno Iskanje", + "desc": "Vnesite ime za to shranjeno iskanje.", + "placeholder": "Vnesite ime za iskanje", + "overwrite": "{{searchName}} že obstaja. Shranjevanje bo prepisalo obstoječo vrednost.", + "success": "Iskanje ({{searchName}}) je bilo shranjeno.", + "button": { + "save": { + "label": "Shrani to iskanje" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potrdi Brisanje", + "desc": { + "selected": "Ali ste prepričani, da želite izbrisati vse posnete videoposnetke, povezane s tem elementom pregleda?

    Držite tipko Shift, da se v prihodnje izognete temu pogovornemu oknu." + }, + "toast": { + "success": "Videoposnetek, povezan z izbranimi elementi pregleda, je bil uspešno izbrisan.", + "error": "Brisanje ni uspelo: {{error}}" + } + }, + "button": { + "export": "Izvoz", + "markAsReviewed": "Označi kot pregledano", + "deleteNow": "Izbriši Zdaj", + "markAsUnreviewed": "Označi kot nepregledano" + } + }, + "imagePicker": { + "selectImage": "Izberite sličico sledenega predmeta", + "search": { + "placeholder": "Iskanje po oznaki ali podoznaki..." + }, + "noImages": "Za to kamero ni bilo najdenih sličic" } } diff --git a/web/public/locales/sl/components/filter.json b/web/public/locales/sl/components/filter.json index b202e1554..5a33b9709 100644 --- a/web/public/locales/sl/components/filter.json +++ b/web/public/locales/sl/components/filter.json @@ -20,7 +20,28 @@ "explore": { "settings": { "defaultView": { - "summary": "Povzetek" + "summary": "Povzetek", + "title": "Privzeti Pogled", + "desc": "Če filtri niso izbrani, prikaži povzetek najnovejših sledenih objektov na oznako ali prikaži nefiltrirano mrežo.", + "unfilteredGrid": "Nefiltrirana Mreža" + }, + "title": "Nastavitve", + "gridColumns": { + "title": "Mrežni Stolpci", + "desc": "Izberite število stolpcev v pogledu mreže." + }, + "searchSource": { + "label": "Iskanje Vira", + "desc": "Izberite, ali želite iskati po sličicah ali opisih sledenih objektov.", + "options": { + "thumbnailImage": "Sličica", + "description": "Opis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Izberite datum za filtriranje" } } }, @@ -30,7 +51,13 @@ }, "sort": { "relevance": "Ustreznost", - "dateAsc": "Datum (naraščajoče)" + "dateAsc": "Datum (naraščajoče)", + "label": "Sortiraj", + "dateDesc": "Datum (Padajoče)", + "scoreAsc": "Ocena Predmeta (Naraščajoče)", + "scoreDesc": "Ocena predmeta (Padajoče)", + "speedAsc": "Ocenjena Hitrost (Naraščajoče)", + "speedDesc": "Ocenjena Hitrost (Padajoče)" }, "zones": { "label": "Cone", @@ -45,7 +72,13 @@ }, "logSettings": { "disableLogStreaming": "Izklopite zapisovanje dnevnika", - "allLogs": "Vsi dnevniki" + "allLogs": "Vsi dnevniki", + "label": "Level Filtra Dnevnika", + "filterBySeverity": "Filtriraj dnevnike po resnosti", + "loading": { + "title": "Nalaganje", + "desc": "Ko se podokno dnevnika pomakne čisto na dno, se novi dnevniki samodejno prikažejo, ko so dodani." + } }, "trackedObjectDelete": { "title": "Potrdite brisanje", @@ -57,5 +90,47 @@ }, "zoneMask": { "filterBy": "Filtrirajte po maski območja" + }, + "classes": { + "label": "Razredi", + "all": { + "title": "Vsi Razredi" + }, + "count_one": "{{count}} Razred", + "count_other": "{{count}} Razredov" + }, + "score": "Ocena", + "estimatedSpeed": "Ocenjena Hitrost ({{unit}})", + "features": { + "label": "Lastnosti", + "hasSnapshot": "Ima sliko", + "hasVideoClip": "Ima posnetek", + "submittedToFrigatePlus": { + "label": "Poslano na Frigate+", + "tips": "Najprej morate filtrirati po sledenih objektih, ki imajo sliko.

    Slednih objektov brez slike ni mogoče poslati v Frigate+." + } + }, + "cameras": { + "label": "Filtri Kamere", + "all": { + "title": "Vse Kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Prikaži Pregledano" + }, + "motion": { + "showMotionOnly": "Prikaži Samo Gibanje" + }, + "recognizedLicensePlates": { + "title": "Prepoznane Registrske Tablice", + "loadFailed": "Prepoznanih registrskih tablic ni bilo mogoče naložiti.", + "loading": "Nalaganje prepoznanih registrskih tablic…", + "placeholder": "Iskanje registrskih tablic…", + "noLicensePlatesFound": "Nobena registrska tablica ni bila najdena.", + "selectPlatesFromList": "Na seznamu izberite eno ali več registrskih tablic.", + "selectAll": "Izberi vse", + "clearAll": "Počisti vse" } } diff --git a/web/public/locales/sl/views/classificationModel.json b/web/public/locales/sl/views/classificationModel.json new file mode 100644 index 000000000..3ba3e676d --- /dev/null +++ b/web/public/locales/sl/views/classificationModel.json @@ -0,0 +1,55 @@ +{ + "description": { + "invalidName": "Neveljavno ime. Ime lahko vsebuje črke, števila, presledke, narekovaje, podčrtaje in pomišljaje." + }, + "categories": "Razredi", + "createCategory": { + "new": "Naredi nov razred" + }, + "button": { + "renameCategory": "Preimenuj razred", + "deleteCategory": "Zbriši razred", + "deleteImages": "Zbriši slike", + "trainModel": "Treniraj model", + "deleteClassificationAttempts": "Izbriši klasifikacijske slike" + }, + "toast": { + "success": { + "deletedCategory": "Izbrisan razred", + "deletedImage": "Zbrisane slike", + "trainedModel": "Uspešno treniranje modela.", + "trainingModel": "Uspešen začetek treniranje modela." + }, + "error": { + "deleteImageFailed": "Neuspešno brisanje: {{errorMessage}}", + "deleteCategoryFailed": "Neuspešno brisanje razreda: {{errorMessage}}", + "trainingFailed": "Neuspešen začetek treniranje modela: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Zbriši razred" + }, + "deleteTrainImages": { + "title": "Zbriši slike za treniranje", + "desc": "Ali ste prepričani, da želite izbrisati {{count}} slik? Tega dejanja ni mogoče razveljaviti." + }, + "renameCategory": { + "title": "Preimenuj razred", + "desc": "Vnesite novo ime za {{name}}. Model bo treba znova naučiti, da bo sprememba imena začela veljati." + }, + "train": { + "title": "Nedavne razvrstitve", + "aria": "Izberi nedavne razvrstitve" + }, + "categorizeImageAs": "Razvrsti sliko kot:", + "categorizeImage": "Razvrsti sliko", + "noModels": { + "object": { + "title": "Ni modelov za razvrščanje objektov" + } + }, + "documentTitle": "Klasifikacijski modeli - fregate", + "details": { + "scoreInfo": "Razultat predstavlja povprečno stopnjo sigurnosti čez vsa zaznavynja objekta." + } +} diff --git a/web/public/locales/sl/views/configEditor.json b/web/public/locales/sl/views/configEditor.json index b8f76525d..5c69cc1b4 100644 --- a/web/public/locales/sl/views/configEditor.json +++ b/web/public/locales/sl/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Napaka pri shranjevanju konfiguracije" } }, - "confirm": "Izhod brez shranjevanja?" + "confirm": "Izhod brez shranjevanja?", + "safeConfigEditor": "Urejevalnik konfiguracij (Varni Način)", + "safeModeDescription": "Frigate je v varnem načinu zaradi napake pri preverjanju konfiguracije." } diff --git a/web/public/locales/sl/views/explore.json b/web/public/locales/sl/views/explore.json index 97e7ca664..70fee301e 100644 --- a/web/public/locales/sl/views/explore.json +++ b/web/public/locales/sl/views/explore.json @@ -3,15 +3,28 @@ "title": "Funkcija razišči ni na voljo", "downloadingModels": { "setup": { - "visionModel": "Model vida" + "visionModel": "Model vida", + "visionModelFeatureExtractor": "Pridobivanje lastnosti modela vida", + "textModel": "Besedilni model", + "textTokenizer": "Tokenizator besedila" }, - "context": "Frigate prenaša potrebne modele vdelave za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti vaše omrežne povezave." + "context": "Frigate prenaša potrebne modele vdelave za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti vaše omrežne povezave.", + "tips": { + "context": "Morda boste želeli ponovno indeksirati vdelave (embeddings) svojih sledenih objektov, ko bodo modeli preneseni.", + "documentation": "Preberi dokumentacijo" + }, + "error": "Prišlo je do napake. Preverite dnevnike Frigate." }, "embeddingsReindexing": { "step": { "descriptionsEmbedded": "Vdelani opisi: ", - "trackedObjectsProcessed": "Obdelani sledeni predmeti: " - } + "trackedObjectsProcessed": "Obdelani sledeni predmeti: ", + "thumbnailsEmbedded": "Vdelane sličice: " + }, + "context": "Funkcija Explore se lahko uporablja, ko je ponovno indeksiranje vgraditev(embeddings) sledenih objektov končano.", + "startingUp": "Zagon…", + "estimatedTime": "Ocenjeni preostali čas:", + "finishingShortly": "Kmalu končano" } }, "documentTitle": "Razišči - Frigate", @@ -29,12 +42,61 @@ "estimatedSpeed": "Ocenjena hitrost", "description": { "placeholder": "Opis sledenega predmeta", - "label": "Opis" + "label": "Opis", + "aiTips": "Frigate od vašega ponudnika generativne UI ne bo zahteval opisa, dokler se življenjski cikel sledenega objekta ne konča." }, "recognizedLicensePlate": "Prepoznana registrska tablica", "objects": "Predmeti", "zones": "Območja", - "timestamp": "Časovni žig" + "timestamp": "Časovni žig", + "item": { + "button": { + "share": "Deli ta element mnenja", + "viewInExplore": "Poglej v Razišči Pogledu" + }, + "tips": { + "hasMissingObjects": "Prilagodite konfiguracijo, če želite, da Frigate shranjuje sledene objekte za naslednje oznake: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Od ponudnika {{provider}} je bil zahtevan nov opis. Glede na hitrost vašega ponudnika lahko regeneracija novega opisa traja nekaj časa.", + "updatedSublabel": "Podoznaka je bila uspešno posodobljena.", + "updatedLPR": "Registrska tablica je bila uspešno posodobljena.", + "audioTranscription": "Zahteva za zvočni prepis je bila uspešno izvedena." + }, + "error": { + "regenerate": "Klic ponudniku {{provider}} za nov opis ni uspel: {{errorMessage}}", + "updatedSublabelFailed": "Posodobitev podoznake ni uspela: {{errorMessage}}", + "updatedLPRFailed": "Posodobitev registrske tablice ni uspela: {{errorMessage}}", + "audioTranscription": "Zahteva za prepis zvoka ni uspela: {{errorMessage}}" + } + }, + "title": "Preglej Podrobnosti Elementa", + "desc": "Preglej podrobnosti elementa" + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi podoznako", + "desc": "Vnesite novo podoznako za {{label}}", + "descNoLabel": "Vnesite novo podoznako za ta sledeni objekt" + }, + "editLPR": { + "title": "Uredi registrsko tablico", + "desc": "Vnesite novo vrednost registrske tablice za {{label}}", + "descNoLabel": "Vnesite novo vrednost registrske tablice za ta sledeni objekt" + }, + "snapshotScore": { + "label": "Ocena Slike" + }, + "topScore": { + "label": "Najboljša Ocena", + "info": "Najboljša ocena je najvišji mediani rezultat za sledeni objekt, zato se lahko razlikuje od rezultata, prikazanega na sličici rezultata iskanja." + }, + "expandRegenerationMenu": "Razširi meni regeneracije", + "tips": { + "descriptionSaved": "Opis uspešno shranjen", + "saveDescriptionFailed": "Opisa ni bilo mogoče posodobiti: {{errorMessage}}" + } }, "itemMenu": { "findSimilar": { @@ -63,11 +125,85 @@ "downloadSnapshot": { "label": "Prenesi posnetek", "aria": "Prenesi posnetek" + }, + "addTrigger": { + "label": "Dodaj sprožilec", + "aria": "Dodaj sprožilec za ta sledeni objekt" + }, + "audioTranscription": { + "label": "Prepis", + "aria": "Zahtevajte prepis zvoka" } }, "dialog": { "confirmDelete": { "title": "Potrdi brisanje" } + }, + "trackedObjectDetails": "Podrobnosti Sledenega Objekta", + "type": { + "details": "podrobnosti", + "snapshot": "posnetek", + "video": "video", + "object_lifecycle": "življenjski cikel objekta" + }, + "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 je bil uspešno izbrisan.", + "error": "Brisanje sledenega predmeta ni uspelo: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/sl/views/faceLibrary.json b/web/public/locales/sl/views/faceLibrary.json index d59acc47e..c41520fa5 100644 --- a/web/public/locales/sl/views/faceLibrary.json +++ b/web/public/locales/sl/views/faceLibrary.json @@ -1,22 +1,28 @@ { "description": { - "addFace": "Sprehodite se skozi dodajanje nove zbirke v knjižnico obrazov.", + "addFace": "Dodajanje nove zbirke v knjižnico obrazov z nalaganjem slike.", "placeholder": "Vnesite ime za to zbirko", "invalidName": "Neveljavno ime. Ime lahko vsebuje črke, števila, presledke, narekovaje, podčrtaje in pomišljaje." }, "details": { "person": "Oseba", "unknown": "Nenznano", - "timestamp": "Časovni žig" + "timestamp": "Časovni žig", + "subLabelScore": "Ocena Podoznake", + "scoreInfo": "Rezultat podoznake je utežena ocena vseh stopenj gotovosti prepoznanih obrazov, zato se lahko razlikuje od ocene, prikazane na posnetku.", + "face": "Podrobnosti Obraza", + "faceDesc": "Podrobnosti sledenega objekta, ki je ustvaril ta obraz" }, "uploadFaceImage": { - "title": "Naloži nov obraz" + "title": "Naloži nov obraz", + "desc": "Naloži sliko za iskanje obrazov in vključitev v {{pageToggle}}" }, "deleteFaceAttempts": { "desc_one": "Ali ste prepričani, da želite izbrisati {{count}} obraz? Tega dejanja ni mogoče razveljaviti.", "desc_two": "Ali ste prepričani, da želite izbrisati {{count}} obraza? Tega dejanja ni mogoče razveljaviti.", "desc_few": "Ali ste prepričani, da želite izbrisati {{count}} obraze? Tega dejanja ni mogoče razveljaviti.", - "desc_other": "Ali ste prepričani, da želite izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti." + "desc_other": "Ali ste prepričani, da želite izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti.", + "title": "Izbriši Obraze" }, "toast": { "success": { @@ -27,8 +33,73 @@ "deletedName_one": "{{count}} 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." + "deletedName_other": "{{count}} obrazov je bilo uspešno izbrisanih.", + "uploadedImage": "Slika je bila uspešno naložena.", + "addFaceLibrary": "Oseba {{name}} je bila uspešno dodana v Knjižnico Obrazov!", + "renamedFace": "Obraz uspešno preimenovan v {{name}}", + "trainedFace": "Uspešno treniran obraz.", + "updatedFaceScore": "Ocena obraza je bila uspešno posodobljena." + }, + "error": { + "uploadingImageFailed": "Nalaganje slike ni uspelo: {{errorMessage}}", + "addFaceLibraryFailed": "Neuspešno nastavljanje imena obraza: {{errorMessage}}", + "deleteFaceFailed": "Brisanje ni uspelo: {{errorMessage}}", + "deleteNameFailed": "Brisanje imena ni uspelo: {{errorMessage}}", + "renameFaceFailed": "Preimenovanje obraza ni uspelo: {{errorMessage}}", + "trainFailed": "Treniranje ni uspelo: {{errorMessage}}", + "updateFaceScoreFailed": "Posodobitev ocene obraza ni uspela: {{errorMessage}}" } }, - "documentTitle": "Knjižnica obrazov - Frigate" + "documentTitle": "Knjižnica obrazov - Frigate", + "collections": "Zbirke", + "createFaceLibrary": { + "title": "Ustvari Zbirko", + "desc": "Ustvari novo zbirko", + "new": "Ustvari Nov Obraz", + "nextSteps": "Za vzpoztavitev trdnih osnov:
  • V zavihku Nedavne prepoznave izberi in uporabi slike za učenje vsake zaznane osebe.
  • Za najboljše rezultate se osredotoči na slike, kjer je obraz obrnjen naravnost; izogibaj se slikam, na katerih so obrazi posneti pod kotom.
  • " + }, + "steps": { + "faceName": "Vnesi Ime Obraza", + "uploadFace": "Naloži Sliko Obraza", + "nextSteps": "Naslednji koraki", + "description": { + "uploadFace": "Naložite sliko osebe {{name}}, ki prikazuje obraz (slikan naravnost in ne iz kota). Slike ni treba obrezati samo na obraz." + } + }, + "train": { + "title": "Nedavne prepoznave", + "aria": "Izberite nedavne prepoznave", + "empty": "Ni nedavnih poskusov prepoznavanja obrazov" + }, + "selectItem": "Izberi {{item}}", + "selectFace": "Izberi Obraz", + "deleteFaceLibrary": { + "title": "Izbriši Ime", + "desc": "Ali ste prepričani, da želite izbrisati zbirko {{name}}? S tem boste trajno izbrisali 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": "Ponovna Obdelava Obraza" + }, + "imageEntry": { + "validation": { + "selectImage": "Izberite slikovno datoteko." + }, + "dropActive": "Sliko spustite tukaj…", + "dropInstructions": "Povlecite in spustite ali prilepite sliko sem ali kliknite za izbiro", + "maxSize": "Največja velikost: {{size}}MB" + }, + "nofaces": "Noben obraz ni na voljo", + "pixels": "{{area}}px", + "readTheDocs": "Preberi dokumentacijo", + "trainFaceAs": "Treniraj obraz kot:", + "trainFace": "Treniraj Obraz" } diff --git a/web/public/locales/sl/views/live.json b/web/public/locales/sl/views/live.json index 212137ba7..5b5261828 100644 --- a/web/public/locales/sl/views/live.json +++ b/web/public/locales/sl/views/live.json @@ -9,14 +9,163 @@ "ptz": { "move": { "clickMove": { - "disable": "Onemogoči funkcijo klikni in premakni" + "disable": "Onemogoči funkcijo klikni in premakni", + "label": "Kliknite v okvir, da postavite kamero na sredino", + "enable": "Omogoči premik s klikom" }, "left": { "label": "Premakni PTZ kamero v levo" }, "up": { "label": "Premakni PTZ kamero gor" + }, + "down": { + "label": "Premakni PTZ kamero navzdol" + }, + "right": { + "label": "Premakni PTZ kamero desno" } + }, + "zoom": { + "in": { + "label": "Povečaj PTZ kamero" + }, + "out": { + "label": "Pomanjšaj PTZ kamero" + } + }, + "focus": { + "in": { + "label": "Izostri PTZ kamero" + }, + "out": { + "label": "Razostri PTZ kamero" + } + }, + "frame": { + "center": { + "label": "Kliknite v okvir, da postavite PTZ kamero na sredino" + } + }, + "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": "Utišaj vse kamere", + "disable": "Vklopi Zvok Vsem Kameram" + }, + "detect": { + "enable": "Omogoči Detekcijo", + "disable": "Onemogoči Detekcijo" + }, + "recording": { + "enable": "Omogoči Snemanje", + "disable": "Onemogoči Snemanje" + }, + "snapshots": { + "enable": "Omogoči Slike", + "disable": "Onemogoči Slike" + }, + "audioDetect": { + "enable": "Omogoči Zvočno Detekcijo", + "disable": "Onemogoči Zvočno Detekcijo" + }, + "transcription": { + "enable": "Omogoči Prepisovanje Zvoka v Živo", + "disable": "Onemogoči Prepisovanje Zvoka v Živo" + }, + "autotracking": { + "enable": "Omogoči Samodejno Sledenje", + "disable": "Onemogoči Samodejno Sledenje" + }, + "streamStats": { + "enable": "Prikaži Statistiko Pretočnega Predvajanja", + "disable": "Skrij Statistiko Pretočnega Predvajanja" + }, + "manualRecording": { + "title": "Snemanje na Zahtevo", + "tips": "Začni ročni dogodek na podlagi nastavitev hranjenja posnetkov te kamere.", + "playInBackground": { + "label": "Predvajaj v ozadju", + "desc": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." + }, + "showStats": { + "label": "Prikaži Statistiko", + "desc": "Omogočite to možnost, če želite statistiko pretoka prikazati kot prekrivni sloj na viru kamere." + }, + "debugView": "Pogled za Odpravljanje Napak", + "start": "Začni snemanje na zahtevo", + "started": "Začelo se je ročno snemanje na zahtevo.", + "failedToStart": "Ročnega snemanja na zahtevo ni bilo mogoče začeti.", + "recordDisabledTips": "Ker je snemanje v nastavitvah te kamere onemogočeno ali omejeno, bo shranjena samo slika.", + "end": "Končaj snemanje na zahtevo", + "ended": "Ročno snemanje na zahtevo je končano.", + "failedToEnd": "Ročnega snemanja na zahtevo ni bilo mogoče končati." + }, + "streamingSettings": "Nastavitve Pretakanja", + "notifications": "Obvestila", + "audio": "Zvok", + "suspend": { + "forTime": "Začasno ustavi za: " + }, + "stream": { + "title": "Pretok", + "audio": { + "tips": { + "title": "Zvok mora biti predvajan iz vaše kamere in konfiguriran v go2rtc za ta pretok.", + "documentation": "Preberi Dokumentacijo " + }, + "available": "Za ta pretok je na voljo zvok", + "unavailable": "Zvok za ta pretok ni na voljo" + }, + "twoWayTalk": { + "tips": "Vaša naprava mora podpirati to funkcijo, WebRTC pa mora biti konfiguriran za dvosmerni pogovor.", + "tips.documentation": "Preberi dokumentacijo ", + "available": "Za ta tok je na voljo dvosmerni pogovor", + "unavailable": "Dvosmerni pogovor ni na voljo za ta pretok" + }, + "lowBandwidth": { + "tips": "Pogled v živo je v načinu nizke pasovne širine zaradi napak v nalaganju ali pretoku.", + "resetStream": "Ponastavi pretok" + }, + "playInBackground": { + "label": "Predvajaj v ozadju", + "tips": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." } + }, + "cameraSettings": { + "title": "{{camera}} Nastavitve", + "cameraEnabled": "Kamera Omogočena", + "objectDetection": "Zaznavanje Objektov", + "recording": "Snemanje", + "snapshots": "Slike", + "audioDetection": "Zvočna Detekcija", + "transcription": "Zvočni Prepis", + "autotracking": "Samodejno Sledenje" + }, + "history": { + "label": "Prikaži stare 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 Kamere" + }, + "exitEdit": "Izhod iz Urejanja" } } diff --git a/web/public/locales/sl/views/settings.json b/web/public/locales/sl/views/settings.json index af8f70748..d8eff4e12 100644 --- a/web/public/locales/sl/views/settings.json +++ b/web/public/locales/sl/views/settings.json @@ -8,7 +8,10 @@ "object": "Odpravljanje napak - Frigate", "general": "Splošne Nastavitve - Frigate", "frigatePlus": "Frigate+ Nastavitve - Frigate", - "enrichments": "Nastavitve Obogatitev - Frigate" + "enrichments": "Nastavitve Obogatitev - Frigate", + "motionTuner": "Nastavitev gibanja - Frigate", + "cameraManagement": "Upravljaj kamere - Frigate", + "cameraReview": "Nastavitve pregleda kamer – Frigate" }, "menu": { "ui": "Uporabniški vmesnik", @@ -18,7 +21,12 @@ "debug": "Razhroščevanje", "users": "Uporabniki", "notifications": "Obvestila", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "motionTuner": "Nastavitev Gibanja", + "triggers": "Prožilniki", + "cameraManagement": "Upravljanje", + "cameraReview": "Pregled", + "roles": "Vloge" }, "masksAndZones": { "zones": { @@ -59,12 +67,271 @@ "desc": "Samodejno preklopite na pogled kamere v živo, ko je zaznana aktivnost. Če onemogočite to možnost, se statične slike kamere na nadzorni plošči v živo posodobijo le enkrat na minuto." }, "playAlertVideos": { - "label": "Predvajajte opozorilne videoposnetke" + "label": "Predvajajte opozorilne videoposnetke", + "desc": "Privzeto se nedavna opozorila na nadzorni plošči predvajajo kot kratki ponavljajoči videoposnetki . To možnost onemogočite, če želite, da se v tej napravi/brskalniku prikaže samo statična slika nedavnih opozoril." } }, "storedLayouts": { "title": "Sharnjene Postavitve", - "desc": "Postaviteve kamer v skupini kamer je mogoče povleči/prilagoditi. Položaji so shranjeni v lokalnem pomnilniku vašega brskalnika." + "desc": "Postaviteve kamer v skupini kamer je mogoče povleči/prilagoditi. Položaji so shranjeni v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Postavitve" + }, + "cameraGroupStreaming": { + "title": "Nastavitve Pretakanja Skupine Kamer", + "desc": "Nastavitve pretakanja za vsako skupino kamer so shranjene v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Nastavitve Pretakanja" + }, + "recordingsViewer": { + "title": "Pregledovalnik Posnetkov", + "defaultPlaybackRate": { + "label": "Privzeta Hitrost Predvajanja", + "desc": "Privzeta Hitrost Predvajanja za Shranjene Posnetke." + } + }, + "calendar": { + "title": "Koledar", + "firstWeekday": { + "label": "Prvi dan v tednu", + "desc": "Dan, na katerega se začnejo tedni v koledarju za preglede.", + "sunday": "Nedelja", + "monday": "Ponedeljek" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Shranjena postavitev za {{cameraName}} je bila izbrisana", + "clearStreamingSettings": "Nastavitve pretakanja za vse skupine kamer so bile izbrisane." + }, + "error": { + "clearStoredLayoutFailed": "Shranjene postavitve ni bilo mogoče izbrisati: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nastavitev pretakanja ni bilo mogoče izbrisati: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavitve Obogatitev", + "unsavedChanges": "Neshranjene Spremembe Nastavitev Obogatitev", + "birdClassification": { + "title": "Klasifikacija ptic", + "desc": "Klasifikacija ptic identificira znane ptice z uporabo kvantiziranega Tensorflow modela. Ko je znana ptica prepoznana, se njeno splošno ime doda kot podoznaka. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila." + }, + "semanticSearch": { + "title": "Semantično Iskanje", + "desc": "Semantično iskanje v Frigate vam omogoča iskanje sledenih objektov znotraj vaših pregledov, pri čemer lahko uporabite izvorno sliko, uporabniško določen besedilni opis ali samodejno ustvarjen opis.", + "readTheDocumentation": "Preberi Dokumentacijo", + "reindexNow": { + "label": "Ponovno Indeksiraj Zdaj", + "desc": "Ponovno indeksiranje bo regeneriralo vdelave (embeddings) za vse sledene objekte. Ta postopek se izvaja v ozadju in lahko zelo obremeni vaš procesor ter traja precej časa, odvisno od števila sledenih objektov, ki jih imate.", + "confirmTitle": "Potrdi Ponovno Indeksiranje", + "confirmDesc": "Ali ste prepričani, da želite ponovno indeksirati vse vdelave (embeddings) sledenih objektov? Ta postopek se bo izvajal v ozadju, vendar lahko zelo obremeni vaš procesor in traja kar nekaj časa. Napredek si lahko ogledate na strani Razišči.", + "confirmButton": "Ponovno Indeksiranje", + "success": "Ponovno indeksiranje se je uspešno začelo.", + "alreadyInProgress": "Ponovno indeksiranje je že v teku.", + "error": "Ponovnega indeksiranja ni bilo mogoče začeti: {{errorMessage}}" + }, + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za vdelave (embeddings) semantičnih iskanj.", + "small": { + "title": "majhen", + "desc": "Uporaba načina small uporablja kvantizirano različico modela, ki porabi manj RAM-a in deluje hitreje na procesorju z zelo zanemarljivo razliko v kakovosti vdelave (embedding)." + }, + "large": { + "title": "velik", + "desc": "Uporaba možnosti large uporablja celoten model Jina in se bo, če je mogoče, samodejno izvajal na grafičnem procesorju." + } + } + }, + "faceRecognition": { + "title": "Prepoznavanje Obrazov", + "desc": "Prepoznavanje obrazov omogoča, da se ljudem dodelijo imena, in ko Frigate prepozna njihov obraz, se detekciji dodeli ime kot podoznako. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila.", + "readTheDocumentation": "Preberi Dokumentacijo", + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za prepoznavanje obrazov.", + "small": { + "title": "majhen", + "desc": "Uporaba small uporablja model vdelave (embedding) obrazov FaceNet, ki učinkovito deluje na večini procesorjev." + }, + "large": { + "title": "velik", + "desc": "Uporaba large uporablja model vdelave (embedding) obrazov ArcFace in se bo samodejno zagnala na grafičnem procesorju, če bo to mogoče." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznavanje Registrskih Tablic", + "desc": "Frigate lahko prepozna registrske tablice na vozilih in samodejno doda zaznane znake v polje recognized_license_plate ali znano ime kot podoznako objektom tipa car. Pogost primer uporabe je lahko branje registrskih tablic avtomobilov, ki se ustavijo na dovozu, ali avtomobilov, ki se peljejo mimo po ulici.", + "readTheDocumentation": "Preberi Dokumentacijo" + }, + "restart_required": "Potreben je ponovni zagon (Nastavitve Obogatitve so bile spremenjene)", + "toast": { + "success": "Nastavitve Obogatitev so shranjene. Znova zaženite Frigate, da uveljavite 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 & Zbirka", + "streamConfiguration": "Konfiguracija pretoka", + "validationAndTesting": "Uverjanje in testiranje" + }, + "save": { + "success": "Kamera {{cameraName}} je bila uspešno shranjena.", + "failure": "Napaka pri shranjevanju {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolucija", + "video": "Video", + "audio": "Zvok", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Prosimo, vnesite veljaven URL pretoka", + "testFailed": "Preizkus pretoka ni uspel: {{error}}" + }, + "step1": { + "description": "Vnesite podatke vaše kamere in preizkusite povezavo.", + "cameraName": "Ime kamere", + "cameraNamePlaceholder": "npr. sprednja_vrata ali Pregled zadnjega dvorišča", + "host": "Gostitelj/IP naslov", + "port": "Vrata", + "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": "Po meri URL za pretok", + "brandInformation": "Informacije o znamki", + "brandUrlFormat": "Za kamere z obliko URL-ja RTSP: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://uporabniškoime: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 biti 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 ne-RTSP pretoke kamer je potrebna ročna nastavitev.", + "brands": { + "reolink-rtsp": "RTSP za Reolink ni priporočen. \nV nastavitvah kamere omogočite HTTP in znova zaženite čarovnika." + } + } + }, + "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": "Test pretoka uspešen!", + "testFailed": "Test pretoka spodletel", + "testFailedTitle": "Test spodletel", + "connected": "Povezan", + "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." + } + }, + "step3": { + "description": "Končno preverjanje in analiza pred shranjevanjem nove kamere. Poveži vsak pretok, preden shranjuješ.", + "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." + } } } } diff --git a/web/public/locales/sl/views/system.json b/web/public/locales/sl/views/system.json index 4a19721c0..6562321a2 100644 --- a/web/public/locales/sl/views/system.json +++ b/web/public/locales/sl/views/system.json @@ -7,7 +7,8 @@ "frigate": "Frigate dnevniki - Frigate", "go2rtc": "Go2RTC dnevniki - Frigate", "nginx": "Nginx dnevniki - Frigate" - } + }, + "enrichments": "Statistika Obogatitev - Frigate" }, "logs": { "download": { @@ -23,6 +24,13 @@ "timestamp": "Časovni žig", "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}}" + } } }, "storage": { @@ -100,7 +108,67 @@ "title": "Kamere", "overview": "Pregled", "info": { - "aspectRatio": "razmerje stranic" + "aspectRatio": "razmerje stranic", + "cameraProbeInfo": "{{camera}} Podrobne Informacije Kamere", + "streamDataFromFFPROBE": "Podatki o pretoku se pridobijo z ukazom ffprobe.", + "fetching": "Pridobivanje Podatkov Kamere", + "stream": "Pretok {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Ločljivost:", + "fps": "FPS:", + "unknown": "Neznano", + "audio": "Zvok:", + "error": "Napaka: {{error}}", + "tips": { + "title": "Podrobne Informacije Kamere" + } + }, + "framesAndDetections": "Okvirji / Zaznave", + "label": { + "camera": "kamera", + "detect": "zaznaj", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "zajemanje", + "overallFramesPerSecond": "skupno število sličic na sekundo (FPS)", + "overallDetectionsPerSecond": "skupno število zaznav na sekundo", + "overallSkippedDetectionsPerSecond": "skupno število preskočenih zaznav na sekundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} zajem", + "cameraDetect": "{{camName}} zaznavanje", + "cameraFramesPerSecond": "{{camName}} sličic na sekundo (FPS)", + "cameraDetectionsPerSecond": "{{camName}} detekcij na sekundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih zaznav na sekundo" + }, + "toast": { + "success": { + "copyToClipboard": "Podatki sonde so bili kopirani v odložišče." + }, + "error": { + "unableToProbeCamera": "Ni mogoče preveriti podrobnosti kamere: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Zadnja osvežitev: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoko porabo procesorja FFmpeg ({{ffmpegAvg}} %)", + "detectHighCpuUsage": "{{camera}} ima visoko porabo procesorja za zaznavanje ({{detectAvg}} %)", + "healthy": "Sistem je zdrav", + "reindexingEmbeddings": "Ponovno indeksiranje vdelanih elementov (embeddings) ({{processed}}% končano)", + "cameraIsOffline": "{{camera}} je nedosegljiva", + "detectIsSlow": "{{detect}} je počasen ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je zelo počasen ({{speed}} ms)" + }, + "enrichments": { + "title": "Obogatitve", + "infPerSecond": "Inference Na Sekundo", + "embeddings": { + "face_recognition": "Prepoznavanje Obrazov", + "plate_recognition": "Prepoznavanje Registrskih Tablic", + "face_recognition_speed": "Hitrost Prepoznavanja Obrazov", + "plate_recognition_speed": "Hitrost Prepoznavanja Registrskih Tablic", + "yolov9_plate_detection": "YOLOv9 Zaznavanje Registrskih Tablic" } } } diff --git a/web/public/locales/sr/audio.json b/web/public/locales/sr/audio.json index a9e52ade6..63c1c25f0 100644 --- a/web/public/locales/sr/audio.json +++ b/web/public/locales/sr/audio.json @@ -11,5 +11,7 @@ "whispering": "Šaptanje", "bus": "Autobus", "laughter": "Smeh", - "train": "Voz" + "train": "Voz", + "boat": "Brod", + "crying": "Plač" } diff --git a/web/public/locales/sr/common.json b/web/public/locales/sr/common.json index a68b33248..06557f2ec 100644 --- a/web/public/locales/sr/common.json +++ b/web/public/locales/sr/common.json @@ -27,5 +27,6 @@ "year_few": "2,3,4,22,23,24,32,33,34,42,...", "year_other": "", "mo": "{{time}}mes" - } + }, + "readTheDocumentation": "Прочитајте документацију" } diff --git a/web/public/locales/sr/components/auth.json b/web/public/locales/sr/components/auth.json index f601ec61a..ecaa132ac 100644 --- a/web/public/locales/sr/components/auth.json +++ b/web/public/locales/sr/components/auth.json @@ -7,7 +7,9 @@ "usernameRequired": "Korisničko ime je obavezno", "passwordRequired": "Lozinka je obavezna", "rateLimit": "Prekoračeno ograničenje brzine. Pokušajte ponovo kasnije.", - "loginFailed": "Prijava nije uspela" + "loginFailed": "Prijava nije uspela", + "unknownError": "Nepoznata greška. Proveri logove.", + "webUnknownError": "Nepoznata greška. Proveri logove u konzoli." } } } diff --git a/web/public/locales/sr/components/camera.json b/web/public/locales/sr/components/camera.json index 6be8272ec..1bb6c3020 100644 --- a/web/public/locales/sr/components/camera.json +++ b/web/public/locales/sr/components/camera.json @@ -11,7 +11,11 @@ } }, "name": { - "label": "Ime" + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Naziv grupe kamera mora imati bar 2 karaktera." + } } } } diff --git a/web/public/locales/sr/components/dialog.json b/web/public/locales/sr/components/dialog.json index 8c5a7c1c4..ead50e869 100644 --- a/web/public/locales/sr/components/dialog.json +++ b/web/public/locales/sr/components/dialog.json @@ -13,6 +13,11 @@ "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}}?" + } } } } diff --git a/web/public/locales/sr/components/filter.json b/web/public/locales/sr/components/filter.json index e00ac754d..d7b8323f6 100644 --- a/web/public/locales/sr/components/filter.json +++ b/web/public/locales/sr/components/filter.json @@ -10,6 +10,10 @@ "count_other": "{{count}} Oznake" }, "zones": { - "label": "Zone" + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } } } diff --git a/web/public/locales/sr/objects.json b/web/public/locales/sr/objects.json index 75f353ded..4edf4728b 100644 --- a/web/public/locales/sr/objects.json +++ b/web/public/locales/sr/objects.json @@ -5,5 +5,6 @@ "motorcycle": "Motor", "airplane": "Avion", "bus": "Autobus", - "train": "Voz" + "train": "Voz", + "boat": "Brod" } diff --git a/web/public/locales/sr/views/classificationModel.json b/web/public/locales/sr/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/sr/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/sr/views/events.json b/web/public/locales/sr/views/events.json index 8a1b76e45..4097e5666 100644 --- a/web/public/locales/sr/views/events.json +++ b/web/public/locales/sr/views/events.json @@ -8,6 +8,7 @@ "allCameras": "Sve Kamere", "empty": { "alert": "Nema upozorenja za pregled", - "detection": "Nema detekcija za pregled" + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu" } } diff --git a/web/public/locales/sr/views/exports.json b/web/public/locales/sr/views/exports.json index a12e06163..ff71c75d5 100644 --- a/web/public/locales/sr/views/exports.json +++ b/web/public/locales/sr/views/exports.json @@ -6,6 +6,7 @@ "deleteExport.desc": "Da li zaista želite obrisati {{exportName}}?", "editExport": { "title": "Preimenuj izvoz", - "desc": "Unesite novo ime za ovaj izvoz." + "desc": "Unesite novo ime za ovaj izvoz.", + "saveExport": "Sačuvaj izvoz" } } diff --git a/web/public/locales/sr/views/faceLibrary.json b/web/public/locales/sr/views/faceLibrary.json index 766a52aa9..c2aa8367b 100644 --- a/web/public/locales/sr/views/faceLibrary.json +++ b/web/public/locales/sr/views/faceLibrary.json @@ -7,6 +7,8 @@ "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." + "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" } } diff --git a/web/public/locales/sr/views/live.json b/web/public/locales/sr/views/live.json index fe19046a3..1374fe163 100644 --- a/web/public/locales/sr/views/live.json +++ b/web/public/locales/sr/views/live.json @@ -7,6 +7,14 @@ "disable": "Onemogućite dvosmerni razgovor" }, "cameraAudio": { - "enable": "Omogući zvuk kamere" + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite na sliku da bi centrirali kameru" + } + } } } diff --git a/web/public/locales/sr/views/search.json b/web/public/locales/sr/views/search.json index 3ab007f60..d72036c66 100644 --- a/web/public/locales/sr/views/search.json +++ b/web/public/locales/sr/views/search.json @@ -5,6 +5,8 @@ "button": { "clear": "Obriši pretragu", "save": "Sačuvaj pretragu", - "delete": "Izbrišite sačuvanu pretragu" + "delete": "Izbrišite sačuvanu pretragu", + "filterInformation": "Filtriraj informacije", + "filterActive": "Aktivni filteri" } } diff --git a/web/public/locales/sr/views/settings.json b/web/public/locales/sr/views/settings.json index 07a4ea59d..2957af0f2 100644 --- a/web/public/locales/sr/views/settings.json +++ b/web/public/locales/sr/views/settings.json @@ -5,6 +5,7 @@ "camera": "Podešavanje kamera - Frigate", "enrichments": "Podešavanja obogaćivanja - Frigate", "masksAndZones": "Uređivač maski i zona - Frigate", - "motionTuner": "Tjuner pokreta - Frigate" + "motionTuner": "Tjuner pokreta - Frigate", + "general": "Generalna podešavanja - Frigate" } } diff --git a/web/public/locales/sr/views/system.json b/web/public/locales/sr/views/system.json index 07f260401..5cd6faa23 100644 --- a/web/public/locales/sr/views/system.json +++ b/web/public/locales/sr/views/system.json @@ -6,7 +6,9 @@ "enrichments": "Statistika obogaćivanja - Frigate", "logs": { "frigate": "Frigate logovi - Frigate", - "go2rtc": "Go2RTC dnevnici - Frigate" + "go2rtc": "Go2RTC dnevnici - Frigate", + "nginx": "Nginx logovi - Frigate" } - } + }, + "title": "Sistem" } diff --git a/web/public/locales/sv/audio.json b/web/public/locales/sv/audio.json index 2e685096c..2de942a50 100644 --- a/web/public/locales/sv/audio.json +++ b/web/public/locales/sv/audio.json @@ -3,7 +3,7 @@ "bicycle": "Cykel", "speech": "Tal", "car": "Bil", - "bellow": "Under", + "bellow": "Vrål", "motorcycle": "Motorcykel", "whispering": "Viskning", "bus": "Buss", @@ -150,7 +150,7 @@ "vehicle": "Fordon", "skateboard": "Skatebord", "door": "Dörr", - "blender": "Mixer", + "blender": "Blandare", "sink": "Vask", "hair_dryer": "Hårfön", "toothbrush": "Tandborste", @@ -158,5 +158,346 @@ "strum": "Anslag", "zither": "Citer", "ukulele": "Ukulele", - "piano": "Piano" + "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 index a220db585..6c9143c9d 100644 --- a/web/public/locales/sv/common.json +++ b/web/public/locales/sv/common.json @@ -16,10 +16,10 @@ "pm": "pm", "am": "am", "yr": "{{time}}år", - "mo": "{{time}} mån", + "mo": "{{time}}må", "month_one": "{{time}} månad", "month_other": "{{time}} månader", - "d": "{{time}}dag", + "d": "{{time}}d", "last7": "Senaste 7 dagarna", "5minutes": "5 minuter", "last30": "Senaste 30 dagarna", @@ -35,13 +35,13 @@ "12hour": "d MMM, yyyy", "24hour": "d MMM, yyy" }, - "h": "{{time}} h", + "h": "{{time}}t", "hour_one": "{{time}} timme", "hour_other": "{{time}} timmar", - "m": "{{time}} m", + "m": "{{time}}m", "minute_one": "{{time}} minut", "minute_other": "{{time}} minuter", - "s": "{{time}} s", + "s": "{{time}}s", "formattedTimestamp": { "12hour": "d MMM, kl. h:mm:ss a", "24hour": "d MMM, HH:mm:ss" @@ -72,7 +72,10 @@ "24hour": "dd-MM-yy-HH-mm-ss" }, "day_one": "{{time}} dag", - "day_other": "{{time}} dagar" + "day_other": "{{time}} dagar", + "inProgress": "Pågår", + "invalidStartTime": "Ogiltig starttid", + "invalidEndTime": "Ogiltig sluttid" }, "button": { "save": "Spara", @@ -104,48 +107,58 @@ "cameraAudio": "Kameraljud", "on": "PÅ", "off": "AV", - "delete": "Släng", + "delete": "Radera", "yes": "Ja", "no": "Nej", "download": "Ladda ner", "info": "Info", - "export": "Exportera" + "export": "Exportera", + "continue": "Fortsätta" }, "menu": { "language": { - "yue": "Kantonesiska", - "it": "Italienska", - "fr": "Franska", - "nl": "Nederländska (Dutch)", - "hi": "Hindi", - "pt": "Portugisiska", - "ru": "Ryska", - "pl": "Polska", - "el": "Grekiska", - "sk": "Slovenska", - "tr": "Turkiska", - "uk": "Ukrainska", - "he": "Hebreiska", - "ro": "Romänska", - "hu": "Ungerska", - "fi": "Finska", - "da": "Danska", - "ar": "Arabiska", - "es": "Spanska", - "zhCN": "Kinesiska", - "de": "Tyska", - "ja": "Japanska", - "sv": "Svenska (Swedish)", - "cs": "Tjeckiska (Czech)", + "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": "Vietnamesiska", - "fa": "Persiska", - "th": "Thailändska", + "ko": "한국어 (Koreanska)", + "vi": "Tiếng Việt (Vietnamesiska)", + "fa": "فارسی (Persiska)", + "th": "ไทย (Thailändska)", "withSystem": { "label": "Använd systeminställningarna för språk" }, - "en": "Engelska" + "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)" }, "darkMode": { "withSystem": { @@ -200,7 +213,8 @@ "languages": "Språk", "configurationEditor": "Konfigurationsredigerare", "withSystem": "System", - "appearance": "Utseende" + "appearance": "Utseende", + "classification": "Klassificering" }, "pagination": { "next": { @@ -241,7 +255,13 @@ "copyUrlToClipboard": "Webbadressen har kopierats till urklipp." }, "label": { - "back": "Gå tillbaka" + "back": "Gå tillbaka", + "hide": "Dölj {{item}}", + "show": "Visa {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alla", + "other": "Annat" }, "unit": { "speed": { @@ -251,7 +271,28 @@ "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}}" + "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" + } } diff --git a/web/public/locales/sv/components/auth.json b/web/public/locales/sv/components/auth.json index 8581ffe94..1fcf9092c 100644 --- a/web/public/locales/sv/components/auth.json +++ b/web/public/locales/sv/components/auth.json @@ -10,6 +10,7 @@ "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 index f4a3448db..23de9471f 100644 --- a/web/public/locales/sv/components/camera.json +++ b/web/public/locales/sv/components/camera.json @@ -62,7 +62,8 @@ "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.", diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json index 42af6ea41..2ef0e8814 100644 --- a/web/public/locales/sv/components/dialog.json +++ b/web/public/locales/sv/components/dialog.json @@ -3,21 +3,26 @@ "button": "Starta om", "restarting": { "title": "Frigate startar om", - "content": "Sidan uppdateras om {{countdown}} seconds.", - "button": "Tvinga uppdatering nu" + "content": "Sidan uppdateras om {{countdown}} sekunder.", + "button": "Tvinga omladdning nu" }, "title": "Är du säker på att du vill starta om Frigate?" }, "explore": { "plus": { "submitToPlus": { - "label": "Skicka till Frigate+" + "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 {{label}}?", "ask_an": "Är detta objektet en {{label}}?", - "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Bekräfta denna etikett för Frigate Plus" + }, + "state": { + "submitted": "Inskickad" } } }, @@ -29,7 +34,7 @@ "time": { "fromTimeline": "Välj från tidslinjen", "lastHour_one": "Sista timma", - "lastHour_other": "Sista t {{count}} Timmarna", + "lastHour_other": "Sista {{count}} timmar", "start": { "title": "Start Tid", "label": "Välj Start Tid" @@ -37,12 +42,82 @@ "end": { "title": "Slut Tid", "label": "Välj Sluttid" - } + }, + "custom": "Anpassad" }, "name": { "placeholder": "Ge exporten ett namn" }, "select": "Välj", - "export": "Eksport" + "export": "Eksport", + "selectOrExport": "Välj eller exportera", + "toast": { + "success": "Exporten har startats. Visa filen på exportsidan.", + "error": { + "failed": "Misslyckades med att starta exporten: {{error}}", + "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", + "noVaildTimeSelected": "Inget giltigt tidsintervall valt" + }, + "view": "Visa" + }, + "fromTimeline": { + "saveExport": "Spara export", + "previewExport": "Förhandsgranska export" + } + }, + "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" + } + }, + "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 index c29110cc7..efb50d919 100644 --- a/web/public/locales/sv/components/filter.json +++ b/web/public/locales/sv/components/filter.json @@ -9,17 +9,22 @@ "count_one": "{{count}} Etikett", "count_other": "{{count}} Etiketter" }, - "filter": "Filter", + "filter": "Filtrera", "zones": { "label": "Zoner", "all": { "title": "Alla zoner", - "short": "Soner" + "short": "Zoner" } }, "features": { "hasSnapshot": "Har ögonblicksbild", - "hasVideoClip": "Har ett video klipp" + "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)", @@ -42,12 +47,22 @@ "settings": { "title": "Inställningar", "defaultView": { - "title": "Standard Vy" + "title": "Standard Vy", + "summary": "Sammanfattning", + "desc": "När inga filter är valda, visa en översikt av de senaste spårade objekten per etikett-typ eller visa ett ofiltrerat rutnät.", + "unfilteredGrid": "Ofiltrerat Rutnät" }, "searchSource": { "options": { - "description": "Beskrivning" - } + "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": { @@ -67,11 +82,18 @@ "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." + "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": "Flera filter", "reset": { @@ -81,5 +103,39 @@ "label": "Under kategori", "all": "Alla under kategorier" }, - "estimatedSpeed": "Estimerad hastighet ({{unit}})" + "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": "Filter 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 index e15428582..afdcfb7d9 100644 --- a/web/public/locales/sv/components/icons.json +++ b/web/public/locales/sv/components/icons.json @@ -1,7 +1,7 @@ { "iconPicker": { "search": { - "placeholder": "Sök efter ikon…" + "placeholder": "Sök efter en ikon…" }, "selectIcon": "Välj en ikon" } diff --git a/web/public/locales/sv/components/player.json b/web/public/locales/sv/components/player.json index b41c5dd65..7c6301ca1 100644 --- a/web/public/locales/sv/components/player.json +++ b/web/public/locales/sv/components/player.json @@ -1,5 +1,5 @@ { - "noPreviewFound": "Ingen Förhandsvisning Hittad", + "noPreviewFound": "Ingen förhandsvisning hittad", "noRecordingsFoundForThisTime": "Inga inspelningar hittade för denna tid", "noPreviewFoundFor": "Ingen förhandsvisning hittad för {{cameraName}}", "submitFrigatePlus": { @@ -39,7 +39,7 @@ "decodedFrames": "Avkodade bildrutor:", "droppedFrameRate": "Frekvens för bortfallna bildrutor:" }, - "cameraDisabled": "Kameran är disablead", + "cameraDisabled": "Kameran är inaktiverad", "toast": { "error": { "submitFrigatePlusFailed": "Bildruta har skickats till Frigate+ med misslyckat resultat" diff --git a/web/public/locales/sv/objects.json b/web/public/locales/sv/objects.json index 4b4da2cf9..1e2926ff3 100644 --- a/web/public/locales/sv/objects.json +++ b/web/public/locales/sv/objects.json @@ -80,7 +80,7 @@ "desk": "Skrivbord", "toilet": "Toalett", "tv": "TV", - "laptop": "Laptop", + "laptop": "Bärbar dator", "remote": "Fjärrkontroll", "keyboard": "Tangentbord", "cell_phone": "Mobiltelefon", @@ -89,7 +89,7 @@ "vase": "Vas", "scissors": "Sax", "squirrel": "Ekorre", - "deer": "Hjort", + "deer": "Rådjur", "fox": "Räv", "rabbit": "Kanin", "raccoon": "Tvättbjörn", @@ -110,9 +110,9 @@ "plate": "Tallrik", "door": "Dörr", "oven": "Ugn", - "blender": "Mixer", + "blender": "Blandare", "book": "Bok", - "waste_bin": "Papperskorg", + "waste_bin": "Soptunna", "license_plate": "Nummerplåt", "toothbrush": "Tandborste", "ups": "UPS", diff --git a/web/public/locales/sv/views/classificationModel.json b/web/public/locales/sv/views/classificationModel.json new file mode 100644 index 000000000..5b5c5b77f --- /dev/null +++ b/web/public/locales/sv/views/classificationModel.json @@ -0,0 +1,188 @@ +{ + "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": "Borttagen klass", + "deletedImage": "Raderade 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 index 27409c968..7b96ff9fe 100644 --- a/web/public/locales/sv/views/configEditor.json +++ b/web/public/locales/sv/views/configEditor.json @@ -12,5 +12,7 @@ }, "documentTitle": "Ändra konfiguration - Frigate", "configEditor": "Ändra konfiguration", - "confirm": "Avsluta utan att spara?" + "confirm": "Avsluta utan att spara?", + "safeConfigEditor": "Konfigurationsredigeraren (felsäkert läge)", + "safeModeDescription": "Fregate ä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 index 9536f9b3d..f849a43a2 100644 --- a/web/public/locales/sv/views/events.json +++ b/web/public/locales/sv/views/events.json @@ -9,7 +9,11 @@ "empty": { "alert": "Det finns inga varningar att granska", "detection": "Det finns inga detekteringar att granska", - "motion": "Ingen rörelsedata hittad" + "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", @@ -34,5 +38,30 @@ "markTheseItemsAsReviewed": "Markera dessa objekt som granskade", "detected": "upptäckt", "selected_one": "{{count}} valda", - "selected_other": "{{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 index e8cecd73f..b6355ea2c 100644 --- a/web/public/locales/sv/views/explore.json +++ b/web/public/locales/sv/views/explore.json @@ -5,25 +5,299 @@ "embeddingsReindexing": { "startingUp": "Startar upp…", "estimatedTime": "Beräknad återstående tid:", - "finishingShortly": "Snart klar" + "finishingShortly": "Snart klar", + "context": "Utforskaren kan användas efter inbäddade spårade objekt har slutat återindexerat.", + "step": { + "thumbnailsEmbedded": "Miniatyrbilder inbäddad: ", + "descriptionsEmbedded": "Beskrivningar inbäddade: ", + "trackedObjectsProcessed": "Spårade objekt bearbetad: " + } }, "title": "Utforska är inte tillgänglig", "downloadingModels": { "setup": { - "textModel": "Text modell" + "textModel": "Text modell", + "visionModel": "Visionsmodell", + "visionModelFeatureExtractor": "Funktionsutdragare för visionsmodell", + "textTokenizer": "Texttokeniserare" }, "tips": { - "documentation": "Läs dokumentationen" + "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." + "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" + "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 nummerplåt för denna {{label}}", + "descNoLabel": "Ange ett nytt nummerplåt för detta spårade objekt" + }, + "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": "Erkänd nummerplåt", + "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" + "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": "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": "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 index f5b8f37b5..da2bc1324 100644 --- a/web/public/locales/sv/views/exports.json +++ b/web/public/locales/sv/views/exports.json @@ -13,5 +13,11 @@ "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 index 763f7533c..485dfdd1f 100644 --- a/web/public/locales/sv/views/faceLibrary.json +++ b/web/public/locales/sv/views/faceLibrary.json @@ -4,33 +4,98 @@ "confidence": "Säkerhet", "face": "Ansiktsdetaljer", "timestamp": "tidsstämpel", - "faceDesc": "Detaljer för ansiktet och tillhörande objekt", - "unknown": "Okänt" + "faceDesc": "Detaljer om det spårade objektet som genererade detta ansikte", + "unknown": "Okänd", + "subLabelScore": "Underetikettpoäng", + "scoreInfo": "Underetikettpoängen är den viktade poängen för alla igenkända ansiktskonfidenser, så detta kan skilja sig från poängen som visas på ögonblicksbilden." }, "description": { "placeholder": "Ange ett namn för denna samling", - "addFace": "Gå genom för att lägga till nya ansikte till biblioteket.", - "invalidName": "Felaktigt namn. Namn kan endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck." + "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." }, "documentTitle": "Ansiktsbibliotek - Frigate", "steps": { "faceName": "Ange namn", "uploadFace": "Ladda upp bild på ansikte", - "nextSteps": "Nästa steg" + "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 Träna för att välja och träna på bilder för varje upptäckt person.
  • Fokusera på raka bilder för bästa resultat; undvik träningsbilder som fångar ansikten i vinkel.
  • ", + "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": "Träna" + "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" + "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": "Ansikts poängen har uppdaterats." + }, + "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 index ff6d2a4c2..b0873ae86 100644 --- a/web/public/locales/sv/views/live.json +++ b/web/public/locales/sv/views/live.json @@ -2,8 +2,8 @@ "documentTitle": "Live - Frigate", "documentTitle.withCamera": "{{camera}} - Live - Frigate", "twoWayTalk": { - "enable": "Aktivera Two Way Talk", - "disable": "Avaktivera Two Way Talk" + "enable": "Aktivera tvåvägssamtal", + "disable": "Avaktivera tvåvägssamtal" }, "cameraAudio": { "disable": "Inaktivera kameraljud", @@ -42,7 +42,15 @@ "label": "Klicka i bilden för att centrera PTZ kamera" } }, - "presets": "PTZ kamera förinställningar" + "presets": "PTZ kamera förinställningar", + "focus": { + "in": { + "label": "Fokusera PTZ-kameran in" + }, + "out": { + "label": "Fokusera PTZ-kameran ut" + } + } }, "streamStats": { "enable": "Visa videostatistik", @@ -65,8 +73,8 @@ "disable": "Avaktivera ljudaktivering" }, "autotracking": { - "enable": "Aktivera automatisk panorering", - "disable": "Avaktivera automatisk panorering" + "enable": "Aktivera Autospårning", + "disable": "Avaktivera Autospårning" }, "notifications": "Notifikationer", "audio": "Ljud", @@ -74,8 +82,8 @@ "manualRecording": { "failedToEnd": "Misslyckades med att avsluta manuell vid behov-inspelning.", "started": "Starta manuell inspelning vid behov.", - "title": "Aktivera inspelning vid behov", - "tips": "Starta en manuell händelse enligt denna kameras inställningar för inspelningslagring.", + "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." @@ -98,7 +106,8 @@ "objectDetection": "Objektsdetektering", "recording": "Inspelning", "snapshots": "Ögonblicksbilder", - "autotracking": "Autospårning" + "autotracking": "Autospårning", + "transcription": "Ljudtranskription" }, "effectiveRetainMode": { "modes": { @@ -128,7 +137,7 @@ "forTime": "Pausa för: " }, "stream": { - "title": "Ström (Swedish also use the word 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.", @@ -150,9 +159,31 @@ "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." + } + }, + "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/recording.json b/web/public/locales/sv/views/recording.json index 6e9e231a3..b4bfaf2ec 100644 --- a/web/public/locales/sv/views/recording.json +++ b/web/public/locales/sv/views/recording.json @@ -1,6 +1,6 @@ { "export": "Export", - "filter": "Filter", + "filter": "Filtrera", "calendar": "Kalender", "filters": "Filter", "toast": { diff --git a/web/public/locales/sv/views/search.json b/web/public/locales/sv/views/search.json index 5fd24ab76..2e9f4e007 100644 --- a/web/public/locales/sv/views/search.json +++ b/web/public/locales/sv/views/search.json @@ -43,7 +43,8 @@ "has_clip": "Har klipp", "has_snapshot": "Har Ögonblicksbild", "labels": "Etiketter", - "max_score": "Högsta Poäng" + "max_score": "Högsta Poäng", + "attributes": "Attribut" }, "searchType": { "thumbnail": "Miniatyrbild", diff --git a/web/public/locales/sv/views/settings.json b/web/public/locales/sv/views/settings.json index df1de30e0..2c054001c 100644 --- a/web/public/locales/sv/views/settings.json +++ b/web/public/locales/sv/views/settings.json @@ -2,26 +2,38 @@ "documentTitle": { "camera": "Kamerainställningar - Frigate", "default": "Inställningar - Frigate", - "general": "Allmänna 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" + "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": "Allmänna Inställningar", + "title": "UI inställningar", "liveDashboard": { "automaticLiveView": { "desc": "Automatiskt byte till kamera där aktivitet registreras. Inaktivering av denna inställning gör att en statisk bild visas i Live Panelen som uppdateras en gång per minut.", "label": "Automatisk Live Visning" }, "playAlertVideos": { - "desc": "Som standard visas varningar på Live Panelen som små loopande klipp. Inaktivera denna inställning för att bara visa en statisk bild av nya varningar på denna enhet/webbläsare.", - "label": "Spela Varnings Videos" + "desc": "Som standard visas varningar på Live panelen som små loopande klipp. Inaktivera denna inställning för att bara visa en statisk bild av nya varningar på denna enhet/webbläsare.", + "label": "Spela upp Varnings videor" }, - "title": "Live Panel" + "title": "Live Panel", + "displayCameraNames": { + "label": "Visa alltid kameranamn", + "desc": "Visa alltid kameranamnen i ett chip i instrumentpanelen för livevisning med flera kameror." + }, + "liveFallbackTimeout": { + "label": "Live spelare reserv timeout", + "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", @@ -44,7 +56,8 @@ "firstWeekday": { "sunday": "Söndag", "monday": "Måndag", - "label": "Första Veckodag" + "label": "Första Veckodag", + "desc": "Den dag då veckorna i översynskalendern börjar." }, "title": "Kalender" }, @@ -66,23 +79,1160 @@ "enrichments": { "unsavedChanges": "Osparade Förbättringsinställningar", "birdClassification": { - "title": "Fågel klassificering" + "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" + "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": "Ansikts igenkänning" + }, + "licensePlateRecognition": { + "title": "Nummerplåt Erkännande", + "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": "UI", + "ui": "Användargränssnitt", "cameras": "Kamera Inställningar", "masksAndZones": "Masker / Områden", "users": "Användare", "notifications": "Notifikationer", "frigateplus": "Frigate+", - "enrichments": "Förbättringar" + "enrichments": "Förbättringar", + "motionTuner": "Rörelsemottagare", + "debug": "Felsök", + "triggers": "Utlösare", + "roles": "Roller", + "cameraManagement": "Hantering", + "cameraReview": "Granska" }, "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}} ({{label}})", + "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." + } + } + }, + "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 Motion Tuner ({{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": "Fregate använder dina detektorer ({{detectors}}) för att upptäcka objekt i din kameras 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 8 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": "Begränsat till Live-dashboards, Review, Explore, och Exports bara.", + "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 kamera åtkomstbehörigheter." + }, + "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": "Att skicka till Frigate+ kräver att både snapshots och clean_copy snapshots är aktiverade i din konfiguration.", + "cleanCopyWarning": "Vissa kameror har aktiverade ögonblicksbilder men har ren kopia inaktiverad. Du måste aktivera clean_copy i din ögonblicksbild konfiguration för att kunna skicka bilder från dessa kameror till Frigate+.", + "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": "Stream-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 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": "Tillfälligt aktivera/inaktivera 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": "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}} 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 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 index d10bf2e1d..27eb9b844 100644 --- a/web/public/locales/sv/views/system.json +++ b/web/public/locales/sv/views/system.json @@ -4,9 +4,11 @@ "general": "Allmän statistik - Frigate", "cameras": "Kamerastatistik - Frigate", "logs": { - "frigate": "Frigate loggar - Frigate", - "go2rtc": "Go2RTC Loggar - Frigate" - } + "frigate": "Frigate-loggar - Frigate", + "go2rtc": "Go2RTC loggar - Frigate", + "nginx": "Nginx loggar - Frigate" + }, + "enrichments": "Förbättringsstatistik - Frigate" }, "logs": { "copy": { @@ -32,19 +34,66 @@ } }, "title": "System", - "metrics": "System detaljer", + "metrics": "Systemdetaljer", "general": { "title": "Generellt", "detector": { - "title": "Detektorer" + "title": "Detektorer", + "inferenceSpeed": "Detektorns inferenshastighet", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektorns CPU-användning", + "memoryUsage": "Detektor minnes anvä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" + "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" + "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": { @@ -55,17 +104,105 @@ "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" + "aspectRatio": "bildförhållande", + "cameraProbeInfo": "{{camera}} Kamerasondinformation", + "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" + "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 urkopplad", + "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": "Ansikts inbäddnings hastighet", + "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": "YOLOv9 nummerplåt detekterings hastighet", + "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/ta/audio.json b/web/public/locales/ta/audio.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/audio.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/common.json b/web/public/locales/ta/common.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/common.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/auth.json b/web/public/locales/ta/components/auth.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/camera.json b/web/public/locales/ta/components/camera.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/dialog.json b/web/public/locales/ta/components/dialog.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/filter.json b/web/public/locales/ta/components/filter.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/icons.json b/web/public/locales/ta/components/icons.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/input.json b/web/public/locales/ta/components/input.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/input.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/components/player.json b/web/public/locales/ta/components/player.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/components/player.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/objects.json b/web/public/locales/ta/objects.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/objects.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/classificationModel.json b/web/public/locales/ta/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/configEditor.json b/web/public/locales/ta/views/configEditor.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/events.json b/web/public/locales/ta/views/events.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/events.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/explore.json b/web/public/locales/ta/views/explore.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/exports.json b/web/public/locales/ta/views/exports.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/faceLibrary.json b/web/public/locales/ta/views/faceLibrary.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/live.json b/web/public/locales/ta/views/live.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/live.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/recording.json b/web/public/locales/ta/views/recording.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/search.json b/web/public/locales/ta/views/search.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/search.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/settings.json b/web/public/locales/ta/views/settings.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/ta/views/system.json b/web/public/locales/ta/views/system.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ta/views/system.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/th/common.json b/web/public/locales/th/common.json index c7044976d..b92078797 100644 --- a/web/public/locales/th/common.json +++ b/web/public/locales/th/common.json @@ -246,5 +246,6 @@ "feet": "ฟุต", "meters": "เมตร" } - } + }, + "readTheDocumentation": "อ่านเอกสาร" } diff --git a/web/public/locales/th/components/filter.json b/web/public/locales/th/components/filter.json index aea9fc5a7..5f23f3142 100644 --- a/web/public/locales/th/components/filter.json +++ b/web/public/locales/th/components/filter.json @@ -82,5 +82,8 @@ }, "title": "การตั้งค่า" } + }, + "classes": { + "label": "หมวดหมู่" } } diff --git a/web/public/locales/th/views/classificationModel.json b/web/public/locales/th/views/classificationModel.json new file mode 100644 index 000000000..3181c4e9f --- /dev/null +++ b/web/public/locales/th/views/classificationModel.json @@ -0,0 +1,9 @@ +{ + "documentTitle": "โมเดลการจำแนกประเภท- Frigate", + "details": { + "scoreInfo": "คะแนน (Score) คือค่าเฉลี่ยของความมั่นใจในการจำแนกประเภท (Classification Confidence) จากการตรวจจับวัตถุชิ้นนี้ในทุกๆ ครั้ง" + }, + "description": { + "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น" + } +} diff --git a/web/public/locales/th/views/faceLibrary.json b/web/public/locales/th/views/faceLibrary.json index 4372d09b5..c6ad3e750 100644 --- a/web/public/locales/th/views/faceLibrary.json +++ b/web/public/locales/th/views/faceLibrary.json @@ -43,8 +43,9 @@ }, "collections": "คอลเลกชัน", "description": { - "addFace": "ทำตามวิธีการเพิ่มคอลเลกชันใหม่ไปยังที่เก็บหน้า.", - "placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้" + "addFace": "เพิ่มคอลเลกชันใหม่ไปยังคลังใบหน้า โดยการอัปโหลดรูปภาพแรก", + "placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้", + "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น" }, "toast": { "success": { diff --git a/web/public/locales/th/views/system.json b/web/public/locales/th/views/system.json index 2084d91a3..fd0010fdd 100644 --- a/web/public/locales/th/views/system.json +++ b/web/public/locales/th/views/system.json @@ -55,5 +55,10 @@ "stats": { "cameraIsOffline": "{{camera}} ออฟไลน์", "detectIsVerySlow": "{{detect}} ช้ามาก ({{speed}} มิลลิวินาที)" + }, + "documentTitle": { + "cameras": "ข้อมูลกล้อง - Frigate", + "storage": "สถิติคลังข้อมูล - Frigate", + "general": "สถิติทั่วไป - Frigate" } } diff --git a/web/public/locales/tr/audio.json b/web/public/locales/tr/audio.json index 6364c8dcd..34a6f366f 100644 --- a/web/public/locales/tr/audio.json +++ b/web/public/locales/tr/audio.json @@ -32,7 +32,7 @@ "sheep": "koyun", "train": "tren", "hair_dryer": "saç kurutma makinesi", - "babbling": "aguşlama", + "babbling": "Agulama", "snicker": "kıkırdama", "sigh": "iç çekme", "bellow": "haykırma", @@ -425,5 +425,79 @@ "radio": "radyo", "field_recording": "alan kaydı", "scream": "çığlık", - "jingle_bell": "küçük çan" + "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 index e23b402ca..c38ffa37d 100644 --- a/web/public/locales/tr/common.json +++ b/web/public/locales/tr/common.json @@ -81,7 +81,10 @@ "formattedTimestampMonthDayYear": { "12hour": "d MMM, yyyy", "24hour": "d MMM, yyyy" - } + }, + "inProgress": "Devam ediyor", + "invalidStartTime": "Geçersiz başlangıç zamanı", + "invalidEndTime": "Geçersiz bitiş zamanı" }, "button": { "off": "KAPALI", @@ -101,14 +104,14 @@ "export": "Dışa aktar", "download": "İndir", "edit": "Düzenle", - "fullscreen": "Tam ekran", + "fullscreen": "Tam Ekran", "deleteNow": "Şimdi Sil", "apply": "Uygula", "reset": "Sıfırla", "done": "Bitti", "enabled": "Açık", "save": "Kaydet", - "exitFullscreen": "Tam ekrandan çık", + "exitFullscreen": "Tam Ekrandan Çık", "pictureInPicture": "Pencere içinde pencere", "copyCoordinates": "Koordinatları kopyala", "yes": "Evet", @@ -118,7 +121,8 @@ "cancel": "İptal", "twoWayTalk": "Çift Yönlü Ses", "close": "Kapat", - "delete": "Sil" + "delete": "Sil", + "continue": "Devam Et" }, "menu": { "systemLogs": "Sistem günlükleri", @@ -166,7 +170,15 @@ "ru": "Русский (Rusça)", "yue": "粵語 (Kantonca)", "th": "ไทย (Tayca)", - "ca": "Català (Katalanca)" + "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)" }, "withSystem": "Sistem", "theme": { @@ -211,10 +223,17 @@ "help": "Yardım", "faceLibrary": "Yüz Veritabanı", "systemMetrics": "Sistem metrikleri", - "uiPlayground": "UI Deneme Alanı" + "uiPlayground": "UI Deneme Alanı", + "classification": "Sınıflandırma" }, "label": { - "back": "Geri" + "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", @@ -229,6 +248,14 @@ "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": { @@ -264,5 +291,18 @@ "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" } } diff --git a/web/public/locales/tr/components/auth.json b/web/public/locales/tr/components/auth.json index dbc444b05..b66836bae 100644 --- a/web/public/locales/tr/components/auth.json +++ b/web/public/locales/tr/components/auth.json @@ -10,6 +10,7 @@ "rateLimit": "İstek sınırı aşıldı. Daha sonra tekrar deneyin.", "unknownError": "Bilinmeyen hata. Günlükleri kontrol edin." }, - "user": "Kullanıcı Adı" + "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 index 8471d7c84..7885c2653 100644 --- a/web/public/locales/tr/components/camera.json +++ b/web/public/locales/tr/components/camera.json @@ -51,7 +51,8 @@ }, "placeholder": "Bir yayın seçin", "stream": "Yayın" - } + }, + "birdseye": "Kuş Bakışı" }, "icon": "Simge", "add": "Kamera Grubu Ekle", diff --git a/web/public/locales/tr/components/dialog.json b/web/public/locales/tr/components/dialog.json index acdb8ef1e..35fe45170 100644 --- a/web/public/locales/tr/components/dialog.json +++ b/web/public/locales/tr/components/dialog.json @@ -59,12 +59,13 @@ "export": "Dışa Aktar", "selectOrExport": "Seç veya Dışa Aktar", "toast": { - "success": "Dışa aktarım başladı. Dosyaya /exports klasöründe veya Dışa Aktar sekmesinden ulaşabilirsiniz.", + "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" }, "fromTimeline": { "saveExport": "Dışa Aktarımı Kaydet", @@ -117,7 +118,16 @@ "button": { "export": "Dışa Aktar", "markAsReviewed": "İncelendi olarak işaretle", - "deleteNow": "Şimdi Sil" + "deleteNow": "Şimdi Sil", + "markAsUnreviewed": "Gözden geçirilmedi olarak işaretle" } + }, + "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 index 96565946f..ef178d4a0 100644 --- a/web/public/locales/tr/components/filter.json +++ b/web/public/locales/tr/components/filter.json @@ -108,7 +108,7 @@ "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 Geçmişi 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." + "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.", @@ -116,12 +116,26 @@ "loading": "Tanınan plakalar yükleniyor…", "title": "Tanınan Plakalar", "noLicensePlatesFound": "Plaka bulunamadı.", - "loadFailed": "Tanınan plakalar yüklenemedi." + "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/views/classificationModel.json b/web/public/locales/tr/views/classificationModel.json new file mode 100644 index 000000000..2081188aa --- /dev/null +++ b/web/public/locales/tr/views/classificationModel.json @@ -0,0 +1,188 @@ +{ + "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": "Silinmiş Sınıf", + "deletedImage": "Silinmiş Fotoğraflar", + "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 index c4aa01b6b..32ffdb2cb 100644 --- a/web/public/locales/tr/views/configEditor.json +++ b/web/public/locales/tr/views/configEditor.json @@ -12,5 +12,7 @@ "configEditor": "Yapılandırma Düzenleyicisi", "documentTitle": "Yapılandırma Düzenleyicisi - Frigate", "saveAndRestart": "Kaydet & Yeniden Başlat", - "confirm": "Kaydetmeden çıkılsın mı?" + "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 index 3f363c70f..d0d30072a 100644 --- a/web/public/locales/tr/views/events.json +++ b/web/public/locales/tr/views/events.json @@ -1,11 +1,15 @@ { "camera": "kamera", - "alerts": "Alarmlar", + "alerts": "Uyarılar", "detections": "Tespitler", "empty": { "detection": "İncelenecek tespit öğesi yok", - "alert": "İncelenecek alarm öğesi yok", - "motion": "Hareket verisi bulunamadı" + "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": { @@ -34,5 +38,30 @@ "allCameras": "Tüm Kameralar", "selected_one": "{{count}} seçildi", "selected_other": "{{count}} seçildi", - "detected": "algılandı" + "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 index 485fe9b43..60aa8648c 100644 --- a/web/public/locales/tr/views/explore.json +++ b/web/public/locales/tr/views/explore.json @@ -10,7 +10,7 @@ "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.", + "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." }, @@ -18,12 +18,16 @@ "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." + "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}}" + "updatedLPRFailed": "Plaka güncellenemedi: {{errorMessage}}", + "audioTranscription": "Ses çözümlemesi talep edilemedi: {{errorMessage}}", + "updatedAttributesFailed": "Öznitelikler güncellenemedi: {{errorMessage}}" } } }, @@ -68,6 +72,17 @@ "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", @@ -102,12 +117,14 @@ "trackedObjectDetails": "Takip Edilen Nesne Detayları", "type": { "details": "detaylar", - "object_lifecycle": "nesne geçmişi", + "object_lifecycle": "nesne yaşam döngüsü", "snapshot": "fotoğraf", - "video": "video" + "video": "video", + "thumbnail": "küçük resim", + "tracking_details": "izleme ayrıntıları" }, "objectLifecycle": { - "title": "Nesne Geçmişi", + "title": "Nesne Yaşam Döngüsü", "noImageFound": "Bu zaman damgası için bir resim bulunamadı.", "createObjectMask": "Nesne Maskesi Oluştur", "adjustAnnotationSettings": "Belirteç ayarları", @@ -150,7 +167,7 @@ "next": "Sonraki sayfa", "previous": "Önceki sayfa" }, - "scrollViewTips": "Bu nesnenin geçmişindeki önemli noktaları görmek için kaydırın.", + "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" @@ -182,6 +199,28 @@ "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ı", @@ -193,15 +232,72 @@ "success": "Takip edilen nesne başarıyla silindi." } }, - "tooltip": "Eşleşme: {{type}} (%{{confidence}})" + "tooltip": "Eşleşme: {{type}} (%{{confidence}})", + "previousTrackedObject": "Önceki izlenen nesne", + "nextTrackedObject": "Sonraki izlenen nesne" }, "dialog": { "confirmDelete": { - "desc": "Bu takip edilen nesneyi silmek nesne fotoğrafını, ilişkili gömüyü ve ilişkili yaşam döngüsü kayıtlarını siler. Video kayıt görüntüleri geçmiş görünümünden SİLİNMEYECEKTİR.

    Devam etmek istediğinize emin misiniz?", + "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" + "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 index 3a1d19512..0c8fec129 100644 --- a/web/public/locales/tr/views/exports.json +++ b/web/public/locales/tr/views/exports.json @@ -13,5 +13,11 @@ "renameExportFailed": "Dışa aktarım adlandırılamadı: {{errorMessage}}" } }, - "noExports": "Dışa aktarım bulunamadı" + "noExports": "Dışa aktarım bulunamadı", + "tooltip": { + "shareExport": "Dışa aktarmayı paylaş", + "downloadVideo": "Videoyu İndir", + "editName": "İsmi Düzenle", + "deleteExport": "Dışa Aktarmayı Sil" + } } diff --git a/web/public/locales/tr/views/faceLibrary.json b/web/public/locales/tr/views/faceLibrary.json index 428d6eaf2..1417eff6d 100644 --- a/web/public/locales/tr/views/faceLibrary.json +++ b/web/public/locales/tr/views/faceLibrary.json @@ -2,8 +2,8 @@ "selectItem": "{{item}} seçin", "description": { "placeholder": "Bu koleksiyona bir isim verin", - "addFace": "Yüz Kütüphanesi’ne yeni bir koleksiyon ekleme adımlarını takip edin.", - "invalidName": "Geçersiz isim. İsimlerde yalnızca harf, sayı, boşluk, kesme işareti, tire veya alt çizgi kullanılabilir." + "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." }, "details": { "person": "İnsan", @@ -13,7 +13,7 @@ "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": "Bilinmeyen" + "unknown": "Bilinmiyor" }, "documentTitle": "Yüz Kütüphanesi - Frigate", "uploadFaceImage": { @@ -24,12 +24,13 @@ "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 'Eğit' sekmesinden resimler seçip eğitin.
  • En iyi sonuçlar için doğrudan karşıdan çekilmiş yüz resimlerine odaklanın; açılı yüz resimlerinden kaçının.
  • " + "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": "Eğit", - "aria": "Eğitimi seç", - "empty": "Yakın zamanda yüz tanıma denemesi olmadı" + "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", @@ -49,7 +50,7 @@ "validation": { "selectImage": "Lütfen bir resim dosyası seçin." }, - "dropInstructions": "Bir resmi buraya sürükleyip bırakın ya da tıklayarak 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": { @@ -61,7 +62,7 @@ "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 skoru başarıyla güncellendi.", + "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": { diff --git a/web/public/locales/tr/views/live.json b/web/public/locales/tr/views/live.json index 88f040856..929b27181 100644 --- a/web/public/locales/tr/views/live.json +++ b/web/public/locales/tr/views/live.json @@ -10,7 +10,7 @@ "enable": "Otomatik Takibi Aç" }, "manualRecording": { - "start": "Talep üzerine kaydı başlat", + "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": { @@ -19,11 +19,11 @@ }, "started": "Manuel talep üzerine kayıt başlatıldı.", "failedToStart": "Manuel talep üzerine kayıt başlatılamadı.", - "title": "İsteğe Bağlı Kayıt", - "end": "Talep üzerine kaydı bitir", + "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 tutma ayarları kapsamında manuel olarak bir olay başlatın.", + "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." @@ -52,7 +52,10 @@ "label": "Arka planda oynat", "tips": "Yayını oynatıcı arkadayken de devam ettirmek için bu seçeneği açın." }, - "title": "Yayı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", @@ -60,8 +63,9 @@ "title": "{{camera}} Ayarları", "autotracking": "Otomatik Takip", "cameraEnabled": "Kamera Açık", - "objectDetection": "Nesne Algılama", - "audioDetection": "Ses Algılama" + "objectDetection": "Nesne Tespiti", + "audioDetection": "Ses Algılama", + "transcription": "Ses Çözümlemesi" }, "effectiveRetainMode": { "modes": { @@ -115,14 +119,22 @@ "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" + "enable": "Kamerayı Aç", + "disable": "Kamerayı Kapat" }, "suspend": { "forTime": "Askıya alınma süresi: " @@ -154,5 +166,24 @@ "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." + } } } diff --git a/web/public/locales/tr/views/search.json b/web/public/locales/tr/views/search.json index 059023308..2de2edf47 100644 --- a/web/public/locales/tr/views/search.json +++ b/web/public/locales/tr/views/search.json @@ -19,11 +19,12 @@ "time_range": "Zaman Aralığı", "before": "Önce", "zones": "Alanlar", - "after": "Sonras", + "after": "Sonra", "has_clip": "Klibi var", "min_speed": "Min. Hız", "sub_labels": "Alt Etiketler", - "max_speed": "Maks. Hız" + "max_speed": "Maks. Hız", + "attributes": "Özellikler" }, "searchType": { "description": "Açıklama", diff --git a/web/public/locales/tr/views/settings.json b/web/public/locales/tr/views/settings.json index 590702370..33ffdd792 100644 --- a/web/public/locales/tr/views/settings.json +++ b/web/public/locales/tr/views/settings.json @@ -8,9 +8,11 @@ "motionTuner": "Hareket Algılama Ayarları - Frigate", "frigatePlus": "Frigate+ Ayarları - Frigate", "object": "Hata Ayıklama - Frigate", - "general": "Genel Ayarlar - Frigate", + "general": "Kullanıcı Arayüzü Ayarları – Frigate", "notifications": "Bildirim Ayarları - Frigate", - "enrichments": "Zenginleştirme Ayarları - Frigate" + "enrichments": "Zenginleştirme Ayarları - Frigate", + "cameraManagement": "Kameraları Yönet - Frigate", + "cameraReview": "Kamera İnceleme Ayarları - Frigate" }, "menu": { "masksAndZones": "Maskeler / Alanlar", @@ -22,10 +24,14 @@ "classification": "Sınıflandırma", "debug": "Hata Ayıklama", "cameras": "Kamera Ayarları", - "enrichments": "Zenginleştirmeler" + "enrichments": "Zenginleştirmeler", + "triggers": "Tetikler", + "cameraManagement": "Yönetim", + "cameraReview": "İncele", + "roles": "Roller" }, "general": { - "title": "Genel Ayarlar", + "title": "Kullanıcı Arayüzü Ayarları", "liveDashboard": { "automaticLiveView": { "label": "Otomatik Canlı Görünüm", @@ -33,9 +39,17 @@ }, "playAlertVideos": { "label": "Alarm Videolarını Oynat", - "desc": "Varsayılan olarak canlı görüntü panelinde gösterilen son alarmlar ufak videolar olarak oynatılır. Bu tarayıcı/cihazda video yerine sabit resim göstermek için bu seçeneği kapatın." + "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" + "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.", @@ -177,6 +191,44 @@ "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": { @@ -198,7 +250,8 @@ "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." + "mustNotContainPeriod": "Alan adı nokta içermemelidir.", + "mustHaveAtLeastOneLetter": "Bölge ismi en az bir harf içermelidir." } }, "distance": { @@ -254,7 +307,7 @@ "name": { "inputPlaceHolder": "Bir isim girin…", "title": "İsim", - "tips": "Ad en az 2 karakter olmalı ve bir kamera veya başka bir bölgenin adı olmamalıdır." + "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", @@ -290,7 +343,7 @@ "title": "Hız Alt Sınırı ({{unit}})" }, "toast": { - "success": "Alan ({{zoneName}}) kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + "success": "Bölge ({{zoneName}}) kaydedildi." }, "allObjects": "Bütün Nesneler" }, @@ -310,8 +363,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", - "noName": "Hareket Maskesi kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + "title": "{{polygonName}} kaydedildi.", + "noName": "Hareket Maskesi kaydedildi." } }, "desc": { @@ -339,8 +392,8 @@ "edit": "Nesne Maskesini Düzenle", "toast": { "success": { - "noName": "Nesne Maskesi kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", - "title": "{{polygonName}} kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + "noName": "Nesne Maskesi kaydedildi.", + "title": "{{polygonName}} kaydedildi." } }, "documentTitle": "Nesne Maskesini Düzenle - Frigate", @@ -396,7 +449,7 @@ "regions": { "title": "Tespit Bölgeleri", "desc": "Nesne algılayıcıya gönderilen tespit alanlarını göster", - "tips": "

    Bölge Kutuları


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

    " + "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", @@ -419,7 +472,20 @@ "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." + "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", @@ -449,7 +515,7 @@ "changeRole": "Kullanıcı rolünü değiştir", "deleteUser": "Kullanıcıyı sil", "role": "Rol", - "password": "Parola" + "password": "Parola Sıfırla" }, "dialog": { "form": { @@ -473,7 +539,16 @@ "veryStrong": "Çok Güçlü" }, "notMatch": "Parolalar eşleşmiyor", - "match": "Parolalar eşleşiyor" + "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", @@ -483,7 +558,11 @@ "title": "Yeni Parola" }, "usernameIsRequired": "Kullanıcı adı gereklidir", - "passwordIsRequired": "Parola gereklidir" + "passwordIsRequired": "Parola gereklidir", + "currentPassword": { + "title": "Mevcut Parola", + "placeholder": "Mevcut parolanızı girin" + } }, "createUser": { "title": "Yeni Kullanıcı Oluştur", @@ -501,7 +580,12 @@ "setPassword": "Parola Belirle", "desc": "Bu hesabı güvenli hale getirmek güçlü bir parola belirleyin.", "cannotBeEmpty": "Parola boş olamaz", - "doNotMatch": "Parolalar eşleşmiyor" + "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", @@ -511,12 +595,13 @@ "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." + "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 Belirle" + "updatePassword": "Parola Sıfırla" }, "notification": { "title": "Bildirimler", @@ -679,5 +764,454 @@ "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": "Bu kamera için yapay zekadan nesne tanımlama taleplerini geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameradaki izlenen nesneler için yapay zekadan tanımlar 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 silecek ve bu role sahip tüm kullanıcıları varsayılan 'izleyici' rolüne atayarak kullanıcıların BÜTÜN kameralara erişim sağlamasına neden olacaktır.", + "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 index 9124e3e08..2311ae5cf 100644 --- a/web/public/locales/tr/views/system.json +++ b/web/public/locales/tr/views/system.json @@ -43,19 +43,32 @@ "gpuEncoder": "GPU Kodlayıcı", "title": "Donanım Bilgisi", "npuUsage": "NPU Kullanımı", - "npuMemory": "NPU Bellek 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ı" + "processMemoryUsage": "İşlem Bellek Kullanımı", + "series": { + "go2rtc": "go2rtc", + "recording": "kayıt", + "embeddings": "gömülü vektörler", + "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ığı" + "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" }, @@ -78,6 +91,10 @@ "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": { @@ -134,7 +151,8 @@ "healthy": "Sistem sağlıklı", "detectIsVerySlow": "{{detect}} çok yavaş çalışıyor ({{speed}} ms)", "cameraIsOffline": "{{camera}} çevrimdışı", - "detectIsSlow": "{{detect}} yavaş çalışıyor ({{speed}} ms)" + "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": { @@ -148,10 +166,20 @@ "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" + "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" + "title": "Zenginleştirmeler", + "averageInf": "Ortalama Çıkarım Süresi" }, "logs": { "download": { diff --git a/web/public/locales/uk/audio.json b/web/public/locales/uk/audio.json index e5b27820a..773d5e3a7 100644 --- a/web/public/locales/uk/audio.json +++ b/web/public/locales/uk/audio.json @@ -425,5 +425,79 @@ "whistling": "Свист", "snoring": "Хропіння", "pant": "Задихатися", - "sneeze": "Чхати" + "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 index 029364971..da875f1f8 100644 --- a/web/public/locales/uk/common.json +++ b/web/public/locales/uk/common.json @@ -78,7 +78,10 @@ "formattedTimestampMonthDayYear": { "24hour": "MMM d, yyyy", "12hour": "MMM d, yyyy" - } + }, + "inProgress": "У процесі", + "invalidStartTime": "Недійсний час початку", + "invalidEndTime": "Недійсний час завершення" }, "button": { "exitFullscreen": "Вийти з повноекранного режиму", @@ -115,7 +118,8 @@ "export": "Експортувати", "deleteNow": "Видалити негайно", "next": "Наступне", - "unsuspended": "Відновити дію" + "unsuspended": "Відновити дію", + "continue": "Продовжити" }, "menu": { "language": { @@ -152,7 +156,15 @@ "en": "Англійська", "yue": "粵語 (Кантонська)", "th": "ไทย (Тайська)", - "ca": "Català (Каталанська)" + "ca": "Català (Каталанська)", + "ptBR": "Português brasileiro (Бразильська португальська)", + "sr": "Српски (Сербська)", + "sl": "Slovenščina (Словенська)", + "lt": "Lietuvių (Литовська)", + "bg": "Български (Болгарська)", + "gl": "Galego (Галісійська)", + "id": "Bahasa Indonesia (Індонезійська)", + "ur": "اردو (Урду)" }, "system": "Система", "systemMetrics": "Системна метріка", @@ -178,7 +190,7 @@ }, "restart": "Перезапустити Frigate", "live": { - "title": "Живи", + "title": "Пряма трансляція", "allCameras": "Всi камери", "cameras": { "title": "Камери", @@ -208,8 +220,9 @@ "label": "Використовуйте налаштування системи для світлого або темного режиму" } }, - "appearance": "Поява", - "withSystem": "Система" + "appearance": "Зовнішність", + "withSystem": "Система", + "classification": "Класифікація" }, "unit": { "speed": { @@ -219,10 +232,24 @@ "length": { "feet": "ноги", "meters": "метрів" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/годину", + "mbph": "МБ/годину", + "gbph": "ГБ/годину" } }, "label": { - "back": "Повернутись" + "back": "Повернутись", + "hide": "Приховати {{item}}", + "show": "Показати {{item}}", + "ID": "ID", + "none": "Жоден", + "all": "Усі", + "other": "Інше" }, "toast": { "save": { @@ -262,5 +289,18 @@ "desc": "Сторінка не знайдена", "title": "404" }, - "selectItem": "Вибрати {{item}}" + "selectItem": "Вибрати {{item}}", + "readTheDocumentation": "Прочитати документацію", + "information": { + "pixels": "{{area}}пикс" + }, + "list": { + "two": "{{0}} і {{1}}", + "many": "{{items}}, і {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необов'язково", + "internalID": "Внутрішній ідентифікатор, який Frigate використовує в конфігурації та базі даних" + } } diff --git a/web/public/locales/uk/components/auth.json b/web/public/locales/uk/components/auth.json index 07eaca4e3..4c9f7e282 100644 --- a/web/public/locales/uk/components/auth.json +++ b/web/public/locales/uk/components/auth.json @@ -10,6 +10,7 @@ }, "user": "Iм'я користувача", "password": "Пароль", - "login": "Логiн" + "login": "Логiн", + "firstTimeLogin": "Намагаєтеся вперше увійти? Облікові дані надруковані в журналах Frigate." } } diff --git a/web/public/locales/uk/components/camera.json b/web/public/locales/uk/components/camera.json index 76886a7b8..0836510e1 100644 --- a/web/public/locales/uk/components/camera.json +++ b/web/public/locales/uk/components/camera.json @@ -50,7 +50,8 @@ }, "stream": "Потік", "placeholder": "Виберіть потік" - } + }, + "birdseye": "Бердсай" }, "edit": "Редагувати групу камер", "delete": { diff --git a/web/public/locales/uk/components/dialog.json b/web/public/locales/uk/components/dialog.json index 43cb9bd9b..7ede7901b 100644 --- a/web/public/locales/uk/components/dialog.json +++ b/web/public/locales/uk/components/dialog.json @@ -57,7 +57,8 @@ "endTimeMustAfterStartTime": "Час закінчення повинен бути після часу початку", "noVaildTimeSelected": "Не вибрано допустимий діапазон часу" }, - "success": "Експорт успішно запущено. Файл доступний у теці /exports." + "success": "Експорт успішно розпочато. Перегляньте файл на сторінці експорту.", + "view": "Переглянути" }, "fromTimeline": { "saveExport": "Зберегти експорт", @@ -89,7 +90,8 @@ "button": { "export": "Експорт", "markAsReviewed": "Позначити як переглянуте", - "deleteNow": "Вилучити зараз" + "deleteNow": "Вилучити зараз", + "markAsUnreviewed": "Позначити як непереглянуте" }, "confirmDelete": { "title": "Підтвердити вилучення", @@ -110,5 +112,13 @@ "content": "Цю сторінку буде перезавантажено за {{countdown}} секунд.", "button": "Примусово перезавантажити" } + }, + "imagePicker": { + "selectImage": "Вибір мініатюри відстежуваного об'єкта", + "search": { + "placeholder": "Пошук за міткою або підміткою..." + }, + "noImages": "Для цієї камери не знайдено мініатюр", + "unknownLabel": "Збережене зображення тригера" } } diff --git a/web/public/locales/uk/components/filter.json b/web/public/locales/uk/components/filter.json index 95c01f349..a99867c0c 100644 --- a/web/public/locales/uk/components/filter.json +++ b/web/public/locales/uk/components/filter.json @@ -97,7 +97,7 @@ "score": "Рахунок", "estimatedSpeed": "Розрахункова швидкість ({{unit}})", "review": { - "showReviewed": "Показати переглянув" + "showReviewed": "Показувати переглянуті" }, "motion": { "showMotionOnly": "Показати тiльки рух" @@ -121,6 +121,20 @@ "loading": "Завантаження визнаних номерів…", "placeholder": "Введіть для пошуку номерні знаки…", "noLicensePlatesFound": "Номерних знаків не знайдено.", - "selectPlatesFromList": "Виберіть одну або кілька пластин зі списку." + "selectPlatesFromList": "Виберіть одну або кілька пластин зі списку.", + "selectAll": "Вибрати все", + "clearAll": "Очистити все" + }, + "classes": { + "label": "Заняття", + "all": { + "title": "Усі класи" + }, + "count_one": "Клас {{count}}", + "count_other": "{{count}} Класи" + }, + "attributes": { + "label": "Атрибути класифікації", + "all": "Усі атрибути" } } diff --git a/web/public/locales/uk/views/classificationModel.json b/web/public/locales/uk/views/classificationModel.json new file mode 100644 index 000000000..a96997bc7 --- /dev/null +++ b/web/public/locales/uk/views/classificationModel.json @@ -0,0 +1,193 @@ +{ + "documentTitle": "Моделі класифікації - Frigate", + "button": { + "deleteClassificationAttempts": "Видалити зображення класифікації", + "renameCategory": "Перейменувати клас", + "deleteCategory": "Видалити клас", + "deleteImages": "Видалити зображення", + "trainModel": "Модель поїзда", + "addClassification": "Додати класифікацію", + "deleteModels": "Видалити моделі", + "editModel": "Редагувати модель" + }, + "toast": { + "success": { + "deletedCategory": "Видалений клас", + "deletedImage": "Видалені зображення", + "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 index c9a664113..0e3ef13cb 100644 --- a/web/public/locales/uk/views/configEditor.json +++ b/web/public/locales/uk/views/configEditor.json @@ -12,5 +12,7 @@ "copyConfig": "Скопіювати конфігурацію", "saveOnly": "Тільки зберегти", "configEditor": "Налаштування редактора", - "confirm": "Вийти без збереження?" + "confirm": "Вийти без збереження?", + "safeConfigEditor": "Редактор конфігурації (безпечний режим)", + "safeModeDescription": "Фрегат перебуває в безпечному режимі через помилку перевірки конфігурації." } diff --git a/web/public/locales/uk/views/events.json b/web/public/locales/uk/views/events.json index e84c418ec..5b3c20443 100644 --- a/web/public/locales/uk/views/events.json +++ b/web/public/locales/uk/views/events.json @@ -12,7 +12,11 @@ "empty": { "alert": "Немає попереджень для перегляду", "detection": "Немає ніяких ознак", - "motion": "Даних про рух не знайдено" + "motion": "Даних про рух не знайдено", + "recordingsDisabled": { + "title": "Записи мають бути ввімкнені", + "description": "Елементи рецензування можна створювати для камери, лише якщо для цієї камери ввімкнено запис." + } }, "timeline": "Хронологія", "timeline.aria": "Вибрати хронiку", @@ -34,5 +38,30 @@ "label": "Переглянути нові елементи огляду", "button": "Нові матеріали для перегляду" }, - "detected": "виявлено" + "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 index cdbcdb6ee..0c7863e05 100644 --- a/web/public/locales/uk/views/explore.json +++ b/web/public/locales/uk/views/explore.json @@ -35,7 +35,9 @@ "error": "Не вдалося видалити відстежуваний об'єкт: {{errorMessage}}", "success": "Відстежуваний об'єкт успішно видалено." } - } + }, + "previousTrackedObject": "Попередній відстежуваний об'єкт", + "nextTrackedObject": "Наступний відстежуваний об'єкт" }, "trackedObjectsCount_one": "{{count}} відстежуваний об'єкт ", "trackedObjectsCount_few": "{{count}} відстежувані об'єкти ", @@ -101,12 +103,16 @@ "success": { "updatedLPR": "Номерний знак успішно оновлено.", "updatedSublabel": "Підмітку успішно оновлено.", - "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час." + "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час.", + "audioTranscription": "Запит на аудіотранскрипцію успішно надіслано. Залежно від швидкості вашого сервера Frigate, транскрипція може тривати деякий час.", + "updatedAttributes": "Атрибути успішно оновлено." }, "error": { "regenerate": "Не вдалося звернутися до {{provider}} для отримання нового опису: {{errorMessage}}", "updatedSublabelFailed": "Не вдалося оновити підмітку: {{errorMessage}}", - "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}" + "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}", + "audioTranscription": "Не вдалося надіслати запит на транскрипцію аудіо: {{errorMessage}}", + "updatedAttributesFailed": "Не вдалося оновити атрибути: {{errorMessage}}" } }, "button": { @@ -158,12 +164,23 @@ } }, "expandRegenerationMenu": "Розгорнути меню регенерації", - "regenerateFromSnapshot": "Відновити зі знімка" + "regenerateFromSnapshot": "Відновити зі знімка", + "score": { + "label": "Оцінка" + }, + "editAttributes": { + "title": "Редагувати атрибути", + "desc": "Виберіть атрибути класифікації для цього {{label}}" + }, + "attributes": "Атрибути класифікації", + "title": { + "label": "Назва" + } }, "dialog": { "confirmDelete": { "title": "Підтвердити видалення", - "desc": "Видалення цього відстежуваного об’єкта призведе до видалення знімка, будь-яких збережених вбудованих елементів та будь-яких пов’язаних записів життєвого циклу об’єкта. Записані кадри цього відстежуваного об’єкта в режимі перегляду історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?" + "desc": "Видалення цього відстежуваного об'єкта призведе до видалення знімка, усіх збережених вбудованих даних та усіх пов'язаних записів деталей відстеження. Записані кадри цього відстежуваного об'єкта в режимі перегляду історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?" } }, "itemMenu": { @@ -193,6 +210,28 @@ }, "deleteTrackedObject": { "label": "Видалити цей відстежуваний об'єкт" + }, + "addTrigger": { + "label": "Додати тригер", + "aria": "Додати тригер для цього відстежуваного об'єкта" + }, + "audioTranscription": { + "label": "Транскрибувати", + "aria": "Запит на аудіотранскрипцію" + }, + "viewTrackingDetails": { + "label": "Переглянути деталі відстеження", + "aria": "Показати деталі відстеження" + }, + "showObjectDetails": { + "label": "Показати шлях до об'єкта" + }, + "hideObjectDetails": { + "label": "Приховати шлях до об'єкта" + }, + "downloadCleanSnapshot": { + "label": "Завантажити чистий знімок", + "aria": "Завантажити чистий знімок" } }, "noTrackedObjects": "Відстежуваних об'єктів не знайдено", @@ -203,7 +242,64 @@ "details": "деталі", "snapshot": "знімок", "video": "відео", - "object_lifecycle": "життєвий цикл об'єкта" + "object_lifecycle": "життєвий цикл об'єкта", + "thumbnail": "мініатюра", + "tracking_details": "деталі відстеження" }, - "exploreMore": "Дослідіть більше об'єктів {{label}}" + "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 index 55ee0e3e8..6b4108f4d 100644 --- a/web/public/locales/uk/views/exports.json +++ b/web/public/locales/uk/views/exports.json @@ -13,5 +13,11 @@ "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 index 34f420704..1170e3ee1 100644 --- a/web/public/locales/uk/views/faceLibrary.json +++ b/web/public/locales/uk/views/faceLibrary.json @@ -17,7 +17,7 @@ "trainFailed": "Не вдалося тренуватися: {{errorMessage}}" }, "success": { - "updatedFaceScore": "Оцінку обличчя успішно оновлено.", + "updatedFaceScore": "Оцінку обличчя успішно оновлено до {{name}} ({{score}}).", "deletedName_one": "{{count}} Обличчя успішно видалено.", "deletedName_few": "{{count}} Обличчі успішно видалено.", "deletedName_many": "{{count}} Облич. успішно видалено.", @@ -66,12 +66,12 @@ "selectImage": "Будь ласка, виберіть файл зображення." }, "dropActive": "Скинь зображення сюди…", - "dropInstructions": "Перетягніть зображення сюди або клацніть, щоб вибрати" + "dropInstructions": "Перетягніть або вставте зображення сюди, або клацніть, щоб вибрати" }, "trainFaceAs": "Тренуйте обличчя як:", "trainFace": "Обличчя поїзда", "description": { - "addFace": "Покрокові інструкції з додавання нової колекції до Бібліотеки облич.", + "addFace": "Додайте нову колекцію до Бібліотеки облич, завантаживши своє перше зображення.", "placeholder": "Введіть назву для цієї колекції", "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." }, @@ -83,12 +83,13 @@ "title": "Створити колекцію", "desc": "Створити нову колекцію", "new": "Створити нове обличчя", - "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Навчання», щоб вибрати та навчити зображення для кожної виявленої особи.
  • Для найкращих результатів зосередьтеся на зображеннях, спрямованих прямо в обличчя; уникайте навчальних зображень, які фіксують обличчя під кутом.
  • " + "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Недавні розпізнавання», щоб вибрати та навчити систему розпізнавати зображення для кожної виявленої особи.
  • Для досягнення найкращих результатів зосередьтеся на прямих зображеннях; уникайте навчання зображень, на яких обличчя зняті під кутом.
  • " }, "train": { - "title": "Поїзд", - "aria": "Виберіть поїзд", - "empty": "Немає останніх спроб розпізнавання обличчя" + "title": "Нещодавні визнання", + "aria": "Виберіть нещодавні визнання", + "empty": "Немає останніх спроб розпізнавання обличчя", + "titleShort": "Нещодавні" }, "collections": "Колекції", "deleteFaceAttempts": { diff --git a/web/public/locales/uk/views/live.json b/web/public/locales/uk/views/live.json index 27a8c518a..0b8b405b2 100644 --- a/web/public/locales/uk/views/live.json +++ b/web/public/locales/uk/views/live.json @@ -10,8 +10,8 @@ "label": "Грати у фоновому режимі", "desc": "Увімкніть цей параметр, щоб продовжувати потокове передавання, коли програвач приховано." }, - "tips": "Запустіть ручну подію на основі параметрів збереження запису цієї камери.", - "title": "Запис на вимогу", + "tips": "Завантажте миттєвий знімок або запустіть ручну подію на основі налаштувань збереження запису цієї камери.", + "title": "На-вимогу", "debugView": "Режим зневаджування", "start": "Почати запис за запитом", "failedToStart": "Не вдалося запустити ручний запис на вимогу.", @@ -46,6 +46,9 @@ "lowBandwidth": { "resetStream": "Скинути потік", "tips": "Режим перегляду в реальному часі перемикається в економічний режим через помилки буферизації або потоку." + }, + "debug": { + "picker": "Вибір потоку недоступний у режимі налагодження. У режимі налагодження завжди використовується потік, якому призначено роль виявлення." } }, "muteCameras": { @@ -85,6 +88,14 @@ "center": { "label": "Клацніть у кадрі, щоб відцентрувати камеру PTZ" } + }, + "focus": { + "in": { + "label": "Фокус PTZ-камери" + }, + "out": { + "label": "Вихід PTZ-камери для фокусування" + } } }, "editLayout": { @@ -94,7 +105,7 @@ "label": "Редагувати групу камер" } }, - "documentTitle": "Прямий трансляція - Frigate", + "documentTitle": "Пряма трансляція - Frigate", "documentTitle.withCamera": "{{camera}} - Пряма трансляція - Frigate", "lowBandwidthMode": "Економічний режим", "twoWayTalk": { @@ -142,7 +153,8 @@ "recording": "Записування", "snapshots": "Знімки", "audioDetection": "Виявлення звуку", - "autotracking": "Автотрекiнг" + "autotracking": "Автотрекiнг", + "transcription": "Аудіотранскрипція" }, "history": { "label": "Показати історичні кадри" @@ -154,5 +166,24 @@ "active_objects": "Активні об'єкти" }, "notAllTips": "Ваш {{source}} конфігурацію збереження записів встановлено на режим: {{effectiveRetainMode}}, тому цей запис на вимогу збереже лише сегменти з {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Увімкнути транскрипцію аудіо в реальному часі", + "disable": "Вимкнути транскрипцію аудіо в реальному часі" + }, + "noCameras": { + "title": "Немає налаштованих камер", + "description": "Почніть з підключення камери до Frigate.", + "buttonText": "Додати камеру", + "restricted": { + "title": "Немає Доступних Камер", + "description": "У вас немає дозволу на перегляд будь-яких камер у цій групі." + } + }, + "snapshot": { + "takeSnapshot": "Завантажити миттєвий знімок", + "noVideoSource": "Немає доступного джерела відео для знімка.", + "captureFailed": "Не вдалося зробити знімок.", + "downloadStarted": "Розпочато завантаження знімка." } } diff --git a/web/public/locales/uk/views/search.json b/web/public/locales/uk/views/search.json index 0d8657e3d..052b4c457 100644 --- a/web/public/locales/uk/views/search.json +++ b/web/public/locales/uk/views/search.json @@ -34,7 +34,8 @@ "max_speed": "Максимальна швидкість", "recognized_license_plate": "Розпізнаний номерний знак", "has_clip": "Має клiп", - "has_snapshot": "Має знiмок" + "has_snapshot": "Має знiмок", + "attributes": "Атрибути" }, "searchType": { "thumbnail": "Мініатюра", diff --git a/web/public/locales/uk/views/settings.json b/web/public/locales/uk/views/settings.json index a5e7d511f..661196ec1 100644 --- a/web/public/locales/uk/views/settings.json +++ b/web/public/locales/uk/views/settings.json @@ -86,7 +86,45 @@ "title": "Огляд", "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. " }, - "title": "Налаштування камери" + "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": { @@ -104,8 +142,8 @@ "edit": "Редагувати маску руху", "toast": { "success": { - "title": "{{polygonName}} збережено. Перезапустіть Frigate, щоб застосувати зміни.", - "noName": "Маску руху збережено. Перезапустіть Frigate, щоб застосувати зміни." + "title": "{{polygonName}} збережено.", + "noName": "Маску руху збережено." } }, "label": "Маска руху", @@ -123,7 +161,7 @@ "name": { "inputPlaceHolder": "Введіть назву…", "title": "Ім'я", - "tips": "Назва має містити щонайменше 2 символи та не повинна бути назвою камери чи іншої зони." + "tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони на цій камері." }, "desc": { "title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.", @@ -169,7 +207,7 @@ "desc": "Список об'єктів, що належать до цієї зони." }, "toast": { - "success": "Зону ({{zoneName}}) збережено. Перезапустіть Frigate, щоб застосувати зміни." + "success": "Зону ({{zoneName}}) збережено." } }, "objectMasks": { @@ -192,8 +230,8 @@ }, "toast": { "success": { - "title": "{{polygonName}} збережено. Перезапустіть Frigate, щоб застосувати зміни.", - "noName": "Маску об'єкта збережено. Перезапустіть Frigate, щоб застосувати зміни." + "title": "{{polygonName}} збережено.", + "noName": "Маску об'єкта збережено." } }, "label": "Маски об'єктів" @@ -214,7 +252,8 @@ "mustNotContainPeriod": "Назва зони не повинна містити крапок.", "mustNotBeSameWithCamera": "Назва зони не повинна збігатися з назвою камери.", "mustBeAtLeastTwoCharacters": "Назва зони має містити щонайменше 2 символи.", - "hasIllegalCharacter": "Назва зони містить недопустимі символи." + "hasIllegalCharacter": "Назва зони містить недопустимі символи.", + "mustHaveAtLeastOneLetter": "Назва зони повинна містити щонайменше одну літеру." } }, "polygonDrawing": { @@ -308,7 +347,20 @@ "tips": "

    Поля руху


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

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

    Шляхи


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

    " + }, + "audio": { + "title": "Аудіо", + "noAudioDetections": "Немає виявлення звуку", + "score": "рахунок", + "currentRMS": "Поточне середньоквадратичне значення", + "currentdbFS": "Поточний dbFS" + }, + "openCameraWebUI": "Відкрийте веб-інтерфейс {{camera}}" }, "classification": { "licensePlateRecognition": { @@ -383,7 +435,7 @@ "supportedDetectors": "Підтримувані детектори", "error": "Не вдалося завантажити інформацію про модель", "availableModels": "Доступні моделі", - "trainDate": "Дата поїзда", + "trainDate": "Дата тренування", "baseModel": "Базова модель", "modelSelect": "Тут можна вибрати доступні моделі на Frigate+. Зверніть увагу, що можна вибрати лише моделі, сумісні з вашою поточною конфігурацією детектора.", "title": "Інформація про модель", @@ -426,7 +478,7 @@ "monday": "Понеділок" } }, - "title": "Загальна налаштування", + "title": "Налаштування інтерфейсу користувача", "liveDashboard": { "title": "Панель керування в прямому ефірі", "automaticLiveView": { @@ -436,6 +488,14 @@ "playAlertVideos": { "label": "Відтворити відео зі сповіщеннями", "desc": "За замовчуванням останні сповіщення на панелі керування Live відтворюються як невеликі відеозаписи, що циклічно відтворюються. Вимкніть цю опцію, щоб відображати лише статичне зображення останніх сповіщень на цьому пристрої/у браузері." + }, + "displayCameraNames": { + "label": "Завжди показувати назви камер", + "desc": "Завжди відображати назви камер у чіпі на панелі керування режимом живого перегляду з кількох камер." + }, + "liveFallbackTimeout": { + "label": "Час очікування резервного програвача в реальному часі", + "desc": "Коли високоякісна пряма трансляція з камери недоступна, повернутися до режиму низької пропускної здатності через певну кількість секунд. За замовчуванням: 3." } }, "storedLayouts": { @@ -498,9 +558,11 @@ "classification": "Налаштування класифікації – Фрегат", "masksAndZones": "Редактор масок та зон – Фрегат", "motionTuner": "Тюнер руху - Фрегат", - "general": "Основна налаштування – Frigate", + "general": "Основна Статус – Frigate", "frigatePlus": "Налаштування Frigate+ – Frigate", - "enrichments": "Налаштуваннях збагачення – Frigate" + "enrichments": "Налаштуваннях збагачення – Frigate", + "cameraManagement": "Керування камерами - Frigate", + "cameraReview": "Налаштування перегляду камери - Frigate" }, "menu": { "ui": "Інтерфейс користувача", @@ -512,7 +574,11 @@ "debug": "Налагодження", "notifications": "Сповіщення", "frigateplus": "Frigate+", - "enrichments": "Збагачення" + "enrichments": "Збагаченням", + "triggers": "Тригери", + "roles": "Ролі", + "cameraManagement": "Управління", + "cameraReview": "Огляду" }, "dialog": { "unsavedChanges": { @@ -530,7 +596,7 @@ "desc": "Керувати обліковими записами користувачів цього екземпляра Frigate." }, "addUser": "Додати користувача", - "updatePassword": "Оновити пароль", + "updatePassword": "Скинути пароль", "toast": { "success": { "deleteUser": "Користувач {{user}} успішно видалений", @@ -546,7 +612,7 @@ } }, "table": { - "password": "Пароль", + "password": "Скинути пароль", "deleteUser": "Видалити користувача", "username": "Ім'я користувача", "actions": "Дії", @@ -576,6 +642,15 @@ "confirm": { "title": "Підтвердьте пароль", "placeholder": "Підтвердьте пароль" + }, + "show": "Показати пароль", + "hide": "Приховати пароль", + "requirements": { + "title": "Вимоги до пароля:", + "length": "Принаймні 8 символів", + "uppercase": "Принаймні одна велика літера", + "digit": "Принаймні одна цифра", + "special": "Принаймні один спеціальний символ (!@#$%^&*(),.?\":{}|<>)" } }, "newPassword": { @@ -586,7 +661,11 @@ "placeholder": "Введіть новий пароль" }, "usernameIsRequired": "Потрібне ім'я користувача", - "passwordIsRequired": "Потрібен пароль" + "passwordIsRequired": "Потрібен пароль", + "currentPassword": { + "title": "Поточний пароль", + "placeholder": "Введіть свій поточний пароль" + } }, "changeRole": { "roleInfo": { @@ -594,7 +673,8 @@ "intro": "Виберіть відповідну роль для цього користувача:", "adminDesc": "Повний доступ до всіх функцій.", "viewer": "Глядач", - "viewerDesc": "Обмежено лише активними інформаційними панелями, функціями «Огляд», «Дослідження» та «Експорт»." + "viewerDesc": "Обмежено лише активними інформаційними панелями, функціями «Огляд», «Дослідження» та «Експорт».", + "customDesc": "Особлива роль з доступом до певної камери." }, "title": "Змінити роль користувача", "desc": "Оновити дозволи для {{username}}", @@ -616,7 +696,12 @@ "setPassword": "Встановити пароль", "desc": "Створіть надійний пароль для захисту цього облікового запису.", "cannotBeEmpty": "Пароль не може бути порожнім", - "doNotMatch": "Паролі не збігаються" + "doNotMatch": "Паролі не збігаються", + "currentPasswordRequired": "Потрібно ввести поточний пароль", + "incorrectCurrentPassword": "Поточний пароль неправильний", + "passwordVerificationFailed": "Не вдалося перевірити пароль", + "multiDeviceWarning": "На будь-яких інших пристроях, на яких ви ввійшли в систему, потрібно буде повторно ввійти протягом {{refresh_time}}.", + "multiDeviceAdmin": "Ви також можете змусити всіх користувачів негайно повторно автентифікуватися, змінивши свій JWT-секрет." } }, "title": "Користувачі" @@ -681,6 +766,548 @@ "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі тензорного потоку. Коли відомого птаха розпізнано, його загальну назву буде додано як підмітку. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях.", "title": "Класифікація птахів" }, - "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": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери. Якщо вимкнено, для елементів огляду на цій камері не запитуватимуться згенеровані штучним інтелектом описи." + }, + "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 index b1472f7a7..b65616c60 100644 --- a/web/public/locales/uk/views/system.json +++ b/web/public/locales/uk/views/system.json @@ -57,10 +57,20 @@ "text_embedding": "Вбудовування тексту", "face_recognition": "Розпізнавання обличчя", "yolov9_plate_detection_speed": "Швидкість виявлення номерних знаків YOLOv9", - "yolov9_plate_detection": "Виявлення пластин 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": "Висновки за секунду" + "infPerSecond": "Висновки за секунду", + "averageInf": "Середній час висновування" }, "general": { "title": "Загальна", @@ -95,19 +105,32 @@ "toast": { "success": "Інформацію про графічний процесор скопійовано в буфер обміну" } + }, + "intelGpuWarning": { + "title": "Попередження щодо статистики графічного процесора Intel", + "message": "Статистика графічного процесора недоступна", + "description": "Це відома помилка в інструментах звітності статистики графічного процесора Intel (intel_gpu_top), яка неодноразово повертає використання графічного процесора на рівні 0%, навіть у випадках, коли апаратне прискорення та виявлення об'єктів працюють належним чином на (i)GPU. Це не помилка Frigate. Ви можете перезавантажити хост, щоб тимчасово виправити проблему та переконатися, що графічний процесор працює правильно. Це не впливає на продуктивність." } }, "otherProcesses": { "processMemoryUsage": "Використання пам'яті процесу", "processCpuUsage": "Використання процесора процесу", - "title": "Інші процеси" + "title": "Інші процеси", + "series": { + "go2rtc": "go2rtc", + "recording": "запис", + "review_segment": "сегмент огляду", + "embeddings": "вбудовування", + "audio_detector": "аудіодетектор" + } }, "detector": { "temperature": "Температура детектора", "title": "Детектори", "inferenceSpeed": "Швидкість виведення детектора", "cpuUsage": "Використання процесора детектора", - "memoryUsage": "Використання пам'яті детектора" + "memoryUsage": "Використання пам'яті детектора", + "cpuUsageInformation": "Процесор, що використовується для підготовки вхідних та вихідних даних до/з моделей виявлення. Це значення не вимірює використання логічного висновку, навіть якщо використовується графічний процесор або прискорювач." } }, "storage": { @@ -129,7 +152,12 @@ "tips": "Це значення відображає загальний обсяг пам’яті, що використовується записами в базі даних Frigate. Frigate не відстежує використання пам’яті для всіх файлів на вашому диску.", "earliestRecording": "Найдавніший доступний запис:" }, - "title": "Зберігання" + "title": "Зберігання", + "shm": { + "title": "Розподіл спільної пам'яті (SHM)", + "warning": "Поточний розмір SHM, що становить {{total}} МБ, замалий. Збільште його принаймні до {{min_shm}} МБ.", + "readTheDocumentation": "Прочитайте документацію" + } }, "lastRefreshed": "Останнє оновлення: ", "stats": { @@ -139,12 +167,13 @@ "reindexingEmbeddings": "Переіндексація вбудовування (виконано {{processed}}%)", "cameraIsOffline": "{{camera}} не в мережі", "detectIsSlow": "{{detect}} повільний ({{speed}} мс)", - "detectIsVerySlow": "{{detect}} дуже повільний ({{speed}} мс)" + "detectIsVerySlow": "{{detect}} дуже повільний ({{speed}} мс)", + "shmTooLow": "Розмір /dev/shm ({{total}} МБ) слід збільшити щонайменше до {{min}} МБ." }, "documentTitle": { "cameras": "Статистика камер - Фрегат", "storage": "Статистика сховища - Фрегат", - "general": "Загальна статистика - Frigate", + "general": "Основна Статус – Frigate", "enrichments": "Статистика збагачені - Фрегат", "logs": { "frigate": "Фрегатні журнали - Фрегат", diff --git a/web/public/locales/ur/common.json b/web/public/locales/ur/common.json index dbf35b3b6..37ff068c5 100644 --- a/web/public/locales/ur/common.json +++ b/web/public/locales/ur/common.json @@ -34,5 +34,6 @@ "month_other": "{{time}} مہینے", "hour_one": "{{time}} گھنٹہ", "hour_other": "{{time}} گھنٹے" - } + }, + "readTheDocumentation": "دستاویز پڑھیں" } diff --git a/web/public/locales/ur/views/classificationModel.json b/web/public/locales/ur/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ur/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/vi/common.json b/web/public/locales/vi/common.json index af34c5ee3..dea1157d9 100644 --- a/web/public/locales/vi/common.json +++ b/web/public/locales/vi/common.json @@ -75,7 +75,10 @@ "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ệ" }, "menu": { "systemLogs": "Nhật ký hệ thống", @@ -121,7 +124,15 @@ }, "yue": "粵語 (Tiếng Quảng Đông)", "ca": "Català (Tiếng Catalan)", - "th": "ไทย (Tiếng Thái)" + "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", @@ -167,7 +178,8 @@ "explore": "Khám phá", "export": "Xuất", "uiPlayground": "UI Playground", - "faceLibrary": "Thư viện khuôn mặt" + "faceLibrary": "Thư viện khuôn mặt", + "classification": "Phân loại" }, "unit": { "speed": { @@ -177,10 +189,23 @@ "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" + "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", @@ -217,7 +242,8 @@ "export": "Xuất", "deleteNow": "Xóa ngay", "next": "Tiếp theo", - "saving": "Đang lưu…" + "saving": "Đang lưu…", + "continue": "Tiếp tục" }, "toast": { "copyUrlToClipboard": "Đã sao chép liên kết.", @@ -257,5 +283,18 @@ "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}}" + "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 index 3d942b9c2..bc664d59c 100644 --- a/web/public/locales/vi/components/auth.json +++ b/web/public/locales/vi/components/auth.json @@ -10,6 +10,7 @@ "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 index 07617eb47..e67824e7b 100644 --- a/web/public/locales/vi/components/camera.json +++ b/web/public/locales/vi/components/camera.json @@ -49,7 +49,8 @@ "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", diff --git a/web/public/locales/vi/components/dialog.json b/web/public/locales/vi/components/dialog.json index 53b1226b1..b8b2895ea 100644 --- a/web/public/locales/vi/components/dialog.json +++ b/web/public/locales/vi/components/dialog.json @@ -56,7 +56,8 @@ "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 thành công. Xem tệp trong thư mục /exports." + "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", @@ -92,7 +93,8 @@ "button": { "deleteNow": "Xóa ngay", "export": "Xuất", - "markAsReviewed": "Đánh dấu là đã xem xét" + "markAsReviewed": "Đánh dấu là đã xem xét", + "markAsUnreviewed": "Đánh dấu là chưa xem xét" } }, "search": { @@ -108,5 +110,13 @@ "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 index 1570067ab..3678ba1ab 100644 --- a/web/public/locales/vi/components/filter.json +++ b/web/public/locales/vi/components/filter.json @@ -93,7 +93,9 @@ "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." + "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": { @@ -122,5 +124,13 @@ "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/views/classificationModel.json b/web/public/locales/vi/views/classificationModel.json new file mode 100644 index 000000000..5db2c5960 --- /dev/null +++ b/web/public/locales/vi/views/classificationModel.json @@ -0,0 +1,59 @@ +{ + "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": "Lớp Đã Bị Xoá", + "deletedImage": "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." + }, + "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 index a9a0c4f82..a2ffce4a9 100644 --- a/web/public/locales/vi/views/configEditor.json +++ b/web/public/locales/vi/views/configEditor.json @@ -12,5 +12,7 @@ } }, "configEditor": "Trình chỉnh sửa cấu hình", - "documentTitle": "Trình chỉnh sửa - Frigate" + "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 index 4259ab2cc..94b2bc710 100644 --- a/web/public/locales/vi/views/events.json +++ b/web/public/locales/vi/views/events.json @@ -34,5 +34,29 @@ "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" + "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 index 99e4a65d5..7110009ce 100644 --- a/web/public/locales/vi/views/explore.json +++ b/web/public/locales/vi/views/explore.json @@ -60,12 +60,14 @@ "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}}" + "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." + "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": { @@ -115,6 +117,9 @@ "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": { @@ -144,6 +149,28 @@ }, "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": { @@ -176,7 +203,7 @@ }, "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 dữ liệu 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 đố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?", + "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" } }, @@ -188,7 +215,9 @@ "error": "Không thể xóa đối tượng được theo dõi: {{errorMessage}}" } }, - "tooltip": "Khớp {{type}} ở mức {{confidence}}%" + "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", @@ -196,10 +225,67 @@ "details": "chi tiết", "snapshot": "ảnh chụp nhanh", "video": "video", - "object_lifecycle": "vòng đời đối tượng" + "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 " + "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 index 6206f5821..95b3b87c6 100644 --- a/web/public/locales/vi/views/exports.json +++ b/web/public/locales/vi/views/exports.json @@ -13,5 +13,11 @@ "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" } } diff --git a/web/public/locales/vi/views/faceLibrary.json b/web/public/locales/vi/views/faceLibrary.json index e27adcf65..cef8b9da7 100644 --- a/web/public/locales/vi/views/faceLibrary.json +++ b/web/public/locales/vi/views/faceLibrary.json @@ -1,7 +1,7 @@ { "selectItem": "Chọn mục {{item}}", "description": { - "addFace": "Hướng dẫn thêm bộ sưu tập mới vào Thư viện khuôn mặt.", + "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" }, @@ -38,7 +38,7 @@ "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 điểm 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.", @@ -76,15 +76,15 @@ "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 Huấn luyện để 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.
  • ", + "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": "Huấn luyện", + "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 huấn luyện" + "aria": "Chọn các nhận dạng gần đây" }, "selectFace": "Chọn khuôn mặt", "pixels": "{{area}}px", diff --git a/web/public/locales/vi/views/live.json b/web/public/locales/vi/views/live.json index 3e8ab44f6..c238a34c6 100644 --- a/web/public/locales/vi/views/live.json +++ b/web/public/locales/vi/views/live.json @@ -29,6 +29,9 @@ "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": { @@ -71,7 +74,15 @@ "label": "Di chuyển camera PTZ sang phải" } }, - "presets": "Các thiết lập sẵn cho camera PTZ" + "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": { @@ -82,8 +93,8 @@ "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": "Ghi hình theo yêu cầu", - "tips": "Bắt đầu sự kiện ghi hình thủ công dựa trên cài đặt lưu trữ của camera này.", + "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." @@ -142,7 +153,8 @@ "recording": "Ghi hình", "snapshots": "Ảnh chụp", "audioDetection": "Phát hiện âm thanh", - "autotracking": "Tự động theo dõi" + "autotracking": "Tự động theo dõi", + "transcription": "Phiên âm" }, "history": { "label": "Hiện cảnh quay lịch sử" @@ -154,5 +166,24 @@ "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/settings.json b/web/public/locales/vi/views/settings.json index 4f0972425..69b37b837 100644 --- a/web/public/locales/vi/views/settings.json +++ b/web/public/locales/vi/views/settings.json @@ -7,9 +7,11 @@ "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 Chung - Frigate", + "general": "Cài đặt giao diện – Frigate", "frigatePlus": "Cài đặt Frigate+ - Frigate", - "motionTuner": "Bộ tinh chỉnh Chuyển động - 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": { @@ -77,7 +79,7 @@ }, "snapshotConfig": { "table": { - "camera": "Camera", + "camera": "Máy quay", "cleanCopySnapshots": "Ảnh chụp nhanh clean_copy", "snapshots": "Ảnh chụp nhanh" }, @@ -142,6 +144,44 @@ "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": { @@ -212,8 +252,8 @@ "point_other": "{{count}} điểm", "toast": { "success": { - "noName": "Mặt nạ đối tượng đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi.", - "title": "{{polygonName}} đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi." + "noName": "Mặt nạ đối tượng đã được lưu.", + "title": "{{polygonName}} đã được lưu." } }, "label": "Mặt nạ đối tượng", @@ -256,11 +296,11 @@ "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. Khởi động lại Frigate để áp dụng các thay đổi." + "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ự và không được trùng với tên của camera hoặc một vùng khác.", + "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", @@ -293,8 +333,8 @@ "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh.", "toast": { "success": { - "title": "{{polygonName}} đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi.", - "noName": "Mặt nạ chuyển động đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi." + "title": "{{polygonName}} đã được lưu.", + "noName": "Mặt nạ chuyển động đã được lưu." } } }, @@ -381,6 +421,19 @@ "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": { @@ -519,7 +572,15 @@ "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" + "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": { @@ -528,7 +589,7 @@ }, "title": "Trình xem Bản ghi" }, - "title": "Cài đặt Chung" + "title": "Cài đặt giao diện" }, "dialog": { "unsavedChanges": { @@ -607,10 +668,109 @@ "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" + "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": "Camera", + "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 index 31da0a086..bdaffe7b9 100644 --- a/web/public/locales/vi/views/system.json +++ b/web/public/locales/vi/views/system.json @@ -42,7 +42,12 @@ "gpuUsage": "Mức sử dụng GPU", "gpuMemory": "Bộ nhớ GPU", "gpuEncoder": "Bộ mã hóa GPU", - "gpuDecoder": "Bộ giải mã 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", @@ -54,7 +59,8 @@ "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" + "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" }, @@ -77,6 +83,10 @@ "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": { @@ -133,7 +143,8 @@ "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}}%)" + "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": { @@ -147,10 +158,17 @@ "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" + "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" + "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", diff --git a/web/public/locales/yue-Hant/common.json b/web/public/locales/yue-Hant/common.json index 03f4f89b4..a65550366 100644 --- a/web/public/locales/yue-Hant/common.json +++ b/web/public/locales/yue-Hant/common.json @@ -76,6 +76,14 @@ "length": { "feet": "呎", "meters": "米" + }, + "data": { + "kbps": "kB/秒", + "mbps": "MB/秒", + "gbps": "GB/秒", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" } }, "label": { @@ -160,7 +168,15 @@ "he": "עברית (希伯來文)", "yue": "粵語 (廣東話)", "th": "ไทย (泰文)", - "ca": "Català (加泰羅尼亞語)" + "ca": "Català (加泰羅尼亞語)", + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)" }, "appearance": "外觀", "darkMode": { @@ -248,5 +264,9 @@ "documentTitle": "找不到頁面 - Frigate", "desc": "找不到頁面", "title": "404" + }, + "readTheDocumentation": "閱讀文件", + "information": { + "pixels": "{{area}}像素" } } diff --git a/web/public/locales/yue-Hant/components/camera.json b/web/public/locales/yue-Hant/components/camera.json index 80cb5d833..ecfa4638c 100644 --- a/web/public/locales/yue-Hant/components/camera.json +++ b/web/public/locales/yue-Hant/components/camera.json @@ -40,7 +40,8 @@ "audioIsUnavailable": "此串流沒有音訊", "placeholder": "選擇串流來源", "stream": "串流" - } + }, + "birdseye": "鳥瞰" }, "delete": { "confirm": { diff --git a/web/public/locales/yue-Hant/components/dialog.json b/web/public/locales/yue-Hant/components/dialog.json index 775681b07..1a3911048 100644 --- a/web/public/locales/yue-Hant/components/dialog.json +++ b/web/public/locales/yue-Hant/components/dialog.json @@ -106,7 +106,15 @@ "button": { "export": "匯出", "markAsReviewed": "標記為已審查", - "deleteNow": "立即刪除" + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審查" } + }, + "imagePicker": { + "selectImage": "選取追蹤物件縮圖", + "search": { + "placeholder": "以標籤或子標籤搜尋..." + }, + "noImages": "未找到此鏡頭的縮圖" } } diff --git a/web/public/locales/yue-Hant/components/filter.json b/web/public/locales/yue-Hant/components/filter.json index b2de0f6e6..bfdc93576 100644 --- a/web/public/locales/yue-Hant/components/filter.json +++ b/web/public/locales/yue-Hant/components/filter.json @@ -91,7 +91,9 @@ "selectPlatesFromList": "從列表中選取一個或多個車牌。", "placeholder": "輸入以搜尋車牌…", "title": "已識別車牌", - "loadFailed": "載入已識別車牌失敗。" + "loadFailed": "載入已識別車牌失敗。", + "selectAll": "全部選取", + "clearAll": "全部清除" }, "estimatedSpeed": "預計速度({{unit}})", "labels": { @@ -122,5 +124,13 @@ "selectPreset": "選擇預設設定…" }, "more": "更多篩選條件", - "timeRange": "時間範圍" + "timeRange": "時間範圍", + "classes": { + "label": "分類", + "all": { + "title": "所有分類" + }, + "count_one": "{{count}} 個分類", + "count_other": "{{count}} 個分類" + } } 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 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/yue-Hant/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/yue-Hant/views/configEditor.json b/web/public/locales/yue-Hant/views/configEditor.json index 3e23edb7f..5bf9d8a2e 100644 --- a/web/public/locales/yue-Hant/views/configEditor.json +++ b/web/public/locales/yue-Hant/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "儲存設定時出錯" } }, - "confirm": "是否不儲存就離開?" + "confirm": "是否不儲存就離開?", + "safeConfigEditor": "設定編輯器 (安全模式)", + "safeModeDescription": "Frigate 因配置驗證錯誤而進入安全模式。" } diff --git a/web/public/locales/yue-Hant/views/events.json b/web/public/locales/yue-Hant/views/events.json index e9929a350..b5e9dc84d 100644 --- a/web/public/locales/yue-Hant/views/events.json +++ b/web/public/locales/yue-Hant/views/events.json @@ -34,5 +34,7 @@ }, "detections": "偵測", "timeline.aria": "選擇時間線", - "detected": "已偵測" + "detected": "已偵測", + "suspiciousActivity": "可疑行為", + "threateningActivity": "威脅行為" } diff --git a/web/public/locales/yue-Hant/views/explore.json b/web/public/locales/yue-Hant/views/explore.json index 46db41b6f..e3a8c9409 100644 --- a/web/public/locales/yue-Hant/views/explore.json +++ b/web/public/locales/yue-Hant/views/explore.json @@ -101,12 +101,14 @@ "success": { "updatedSublabel": "成功更新子標籤。", "updatedLPR": "成功更新車牌號碼。", - "regenerate": "已從 {{provider}} 請求新的描述。根據提供者的速度,生成新的描述可能需要一些時間。" + "regenerate": "已從 {{provider}} 請求新的描述。根據提供者的速度,生成新的描述可能需要一些時間。", + "audioTranscription": "成功請求音訊轉錄。" }, "error": { "regenerate": "呼叫 {{provider}} 以獲取新描述失敗:{{errorMessage}}", "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", - "updatedLPRFailed": "更新車牌號碼失敗:{{errorMessage}}" + "updatedLPRFailed": "更新車牌號碼失敗:{{errorMessage}}", + "audioTranscription": "請求音訊轉錄失敗:{{errorMessage}}" } } }, @@ -152,7 +154,10 @@ "label": "快照分數" }, "expandRegenerationMenu": "展開重新生成選單", - "regenerateFromThumbnails": "從縮圖重新生成" + "regenerateFromThumbnails": "從縮圖重新生成", + "score": { + "label": "分數" + } }, "itemMenu": { "downloadVideo": { @@ -181,6 +186,14 @@ }, "deleteTrackedObject": { "label": "刪除此追蹤物件" + }, + "addTrigger": { + "label": "新增觸發器", + "aria": "為此追蹤物件新增觸發器" + }, + "audioTranscription": { + "label": "轉錄音訊", + "aria": "請求音訊轉錄" } }, "dialog": { @@ -201,5 +214,11 @@ "tooltip": "已配對{{type}}({{confidence}}% 信心" }, "trackedObjectsCount_other": "{{count}} 個追蹤物件 ", - "exploreMore": "瀏覽更多{{label}}物件" + "exploreMore": "瀏覽更多{{label}}物件", + "aiAnalysis": { + "title": "AI 分析" + }, + "concerns": { + "label": "關注" + } } diff --git a/web/public/locales/yue-Hant/views/faceLibrary.json b/web/public/locales/yue-Hant/views/faceLibrary.json index 2c1e11b24..53525d914 100644 --- a/web/public/locales/yue-Hant/views/faceLibrary.json +++ b/web/public/locales/yue-Hant/views/faceLibrary.json @@ -61,7 +61,7 @@ "selectImage": "請選擇一個圖片檔案。" }, "dropActive": "將圖片拖到這裡…", - "dropInstructions": "拖放圖片到此處,或點擊選取", + "dropInstructions": "拖放圖片或貼上到此處,或點擊選取", "maxSize": "最大檔案大小:{{size}}MB" }, "readTheDocs": "閱讀文件", diff --git a/web/public/locales/yue-Hant/views/live.json b/web/public/locales/yue-Hant/views/live.json index d9dda2630..bb3b440ee 100644 --- a/web/public/locales/yue-Hant/views/live.json +++ b/web/public/locales/yue-Hant/views/live.json @@ -37,6 +37,14 @@ "out": { "label": "縮小 PTZ 鏡頭" } + }, + "focus": { + "in": { + "label": "PTZ 鏡頭拉近焦距" + }, + "out": { + "label": "PTZ 鏡頭拉遠焦距" + } } }, "twoWayTalk": { @@ -66,7 +74,7 @@ "disable": "隱藏串流統計資料" }, "manualRecording": { - "title": "按需錄影", + "title": "按需", "tips": "根據此鏡頭的錄影保留設定手動啟動事件。", "debugView": "除錯視圖", "start": "開始按需錄影", @@ -126,6 +134,9 @@ "playInBackground": { "tips": "啟用此選項可在播放器隱藏時繼續串流播放。", "label": "背景播放" + }, + "debug": { + "picker": "除錯模式下無法選擇串流。除錯視圖永遠使用已分配偵測角色的串流。" } }, "cameraSettings": { @@ -135,7 +146,8 @@ "snapshots": "快照", "autotracking": "自動追蹤", "audioDetection": "音訊偵測", - "title": "{{camera}} 設定" + "title": "{{camera}} 設定", + "transcription": "音訊轉錄" }, "history": { "label": "顯示歷史影像" @@ -154,5 +166,20 @@ "label": "編輯鏡頭群組" }, "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時音訊轉錄", + "disable": "停用即時音訊轉錄" + }, + "noCameras": { + "title": "未設置任何鏡頭", + "description": "連接鏡頭開始使用。", + "buttonText": "新增鏡頭" + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "無可用影片來源以擷取快照。", + "captureFailed": "擷取快照失敗。", + "downloadStarted": "已開始下載快照。" } } diff --git a/web/public/locales/yue-Hant/views/settings.json b/web/public/locales/yue-Hant/views/settings.json index a68c9d2bd..34982abb4 100644 --- a/web/public/locales/yue-Hant/views/settings.json +++ b/web/public/locales/yue-Hant/views/settings.json @@ -10,7 +10,9 @@ "general": "一般設定 - Frigate", "frigatePlus": "Frigate+ 設定 - Frigate", "notifications": "通知設定 - Frigate", - "enrichments": "進階功能設定 - Frigate" + "enrichments": "進階功能設定 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "鏡頭檢視設定 - Frigate" }, "menu": { "ui": "介面", @@ -22,7 +24,11 @@ "users": "用戶", "notifications": "通知", "frigateplus": "Frigate+", - "enrichments": "進階功能" + "enrichments": "進階功能", + "triggers": "觸發器", + "roles": "角色", + "cameraManagement": "管理", + "cameraReview": "審查" }, "dialog": { "unsavedChanges": { @@ -178,6 +184,43 @@ "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": { @@ -417,6 +460,19 @@ "ratio": "比例", "desc": "在圖片上畫矩形以查看面積與比例詳情", "tips": "啟用此選項後,會於鏡頭畫面上繪製矩形,以顯示其面積及比例。這些數值可用於設定物件形狀過濾參數。" + }, + "openCameraWebUI": "打開 {{camera}} 的網頁介面", + "audio": { + "title": "音訊", + "noAudioDetections": "未偵測到音訊", + "score": "分數", + "currentRMS": "目前 RMS", + "currentdbFS": "目前 dbFS" + }, + "paths": { + "title": "軌跡", + "desc": "顯示追蹤物件軌跡上的重要點", + "tips": "

    軌跡


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

    " } }, "users": { @@ -502,7 +558,8 @@ "adminDesc": "可使用所有功能。", "viewer": "觀看者", "viewerDesc": "只限使用即時儀表板、審查、瀏覽及匯出功能。", - "admin": "管理員" + "admin": "管理員", + "customDesc": "自訂角色,具特定鏡頭存取權限。" }, "select": "選擇角色" }, @@ -676,5 +733,386 @@ "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": "發送通知" + }, + "dialog": { + "createTrigger": { + "title": "建立觸發器", + "desc": "為鏡頭 {{camera}} 建立觸發器" + }, + "editTrigger": { + "title": "編輯觸發器", + "desc": "編輯鏡頭 {{camera}} 的觸發器設定" + }, + "deleteTrigger": { + "title": "刪除觸發器", + "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作無法復原。" + }, + "form": { + "name": { + "title": "名稱", + "placeholder": "輸入觸發器名稱", + "error": { + "minLength": "名稱至少需 2 個字元。", + "invalidCharacters": "名稱只可包含字母、數字、底線及連字符。", + "alreadyExists": "此鏡頭已有相同名稱的觸發器。" + } + }, + "enabled": { + "description": "啟用或停用此觸發器" + }, + "type": { + "title": "類型", + "placeholder": "選擇觸發器類型" + }, + "friendly_name": { + "title": "顯示名稱", + "placeholder": "為此觸發器命名或描述", + "description": "此觸發器的可選顯示名稱或描述文字。" + }, + "content": { + "title": "內容", + "imagePlaceholder": "選擇圖片", + "textPlaceholder": "輸入文字內容", + "imageDesc": "選擇圖片,當偵測到相似圖片時觸發此動作。", + "textDesc": "輸入文字,當偵測到相似追蹤物件描述時觸發此動作。", + "error": { + "required": "必須提供內容。" + } + }, + "threshold": { + "title": "閾值", + "error": { + "min": "閾值至少為 0", + "max": "閾值最多為 1" + } + }, + "actions": { + "title": "操作", + "desc": "預設情況下,Frigate 會對所有觸發器發送 MQTT 訊息。可選擇額外操作,在觸發器觸發時執行。", + "error": { + "min": "至少需要選擇一個操作。" + } + } + } + }, + "toast": { + "success": { + "createTrigger": "觸發器 {{name}} 已成功建立。", + "updateTrigger": "觸發器 {{name}} 已成功更新。", + "deleteTrigger": "觸發器 {{name}} 已成功刪除。" + }, + "error": { + "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}", + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}", + "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "新增鏡頭", + "description": "請依照以下步驟,將新鏡頭加入 Frigate。", + "steps": { + "nameAndConnection": "名稱與連線", + "streamConfiguration": "串流設定", + "validationAndTesting": "驗證與測試" + }, + "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,並重新啟動鏡頭設定精靈。" + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "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 轉串流以減少與鏡頭的直接連線。" + } + }, + "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 鏡頭支援額外子串流,需要在鏡頭設定中啟用。建議如有可用,檢查並使用這些子串流。" + } + } + } + }, + "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 物件描述。停用時,系統不會為此鏡頭的追蹤物件生成 AI 描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審查描述", + "desc": "暫時啟用/停用此鏡頭的生成式 AI 審查描述。停用時,系統不會為此鏡頭的審查項目生成 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 index 8accc5bb1..6b52401c8 100644 --- a/web/public/locales/yue-Hant/views/system.json +++ b/web/public/locales/yue-Hant/views/system.json @@ -41,7 +41,8 @@ "memoryUsage": "偵測器記憶體使用量", "title": "偵測器", "cpuUsage": "偵測器 CPU 使用率", - "temperature": "偵測器溫度" + "temperature": "偵測器溫度", + "cpuUsageInformation": "CPU 用於準備偵測模型的輸入同輸出數據。此數值不計算推理運算,即使使用 GPU 或加速器也是一樣。" }, "hardwareInfo": { "gpuUsage": "GPU 使用率", @@ -102,6 +103,10 @@ }, "title": "鏡頭儲存", "percentageOfTotalUsed": "佔總量百分比" + }, + "shm": { + "title": "SHM(共享記憶體) 分配", + "warning": "目前 SHM 大小 {{total}}MB 太小,請增加至至少 {{min_shm}}MB。" } }, "cameras": { @@ -158,7 +163,8 @@ "detectHighCpuUsage": "{{camera}} 的偵測 CPU 使用率過高 ({{detectAvg}}%)", "healthy": "系統運作正常", "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率過高 ({{ffmpegAvg}}%)", - "reindexingEmbeddings": "重新索引嵌入資料 (已完成 {{processed}}%)" + "reindexingEmbeddings": "重新索引嵌入資料 (已完成 {{processed}}%)", + "shmTooLow": "/dev/shm 分配({{total}} MB)太小,請增加至至少 {{min}} MB。" }, "enrichments": { "title": "進階功能", diff --git a/web/public/locales/zh-CN/audio.json b/web/public/locales/zh-CN/audio.json index bd97f632f..848418f84 100644 --- a/web/public/locales/zh-CN/audio.json +++ b/web/public/locales/zh-CN/audio.json @@ -102,7 +102,7 @@ "flapping_wings": "翅膀拍打", "dogs": "狗群", "rats": "老鼠", - "mouse": "老鼠", + "mouse": "鼠标", "patter": "啪嗒声", "insect": "昆虫", "cricket": "蟋蟀", @@ -425,5 +425,79 @@ "television": "电视", "radio": "收音机", "field_recording": "实地录音", - "scream": "尖叫" + "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 index 1c253aee4..42b1f5c65 100644 --- a/web/public/locales/zh-CN/common.json +++ b/web/public/locales/zh-CN/common.json @@ -75,7 +75,10 @@ "formattedTimestampMonthDayYear": { "12hour": "yy年MM月dd日", "24hour": "yy年MM月dd日" - } + }, + "inProgress": "进行中", + "invalidStartTime": "无效的开始时间", + "invalidEndTime": "无效的结束时间" }, "unit": { "speed": { @@ -85,10 +88,24 @@ "length": { "feet": "英尺", "meters": "米" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/每小时", + "mbph": "MB/每小时", + "gbph": "GB/每小时" } }, "label": { - "back": "返回" + "back": "返回", + "hide": "隐藏 {{item}}", + "show": "显示 {{item}}", + "ID": "ID", + "none": "无", + "all": "所有", + "other": "其他" }, "pagination": { "label": "分页", @@ -137,7 +154,8 @@ "deleteNow": "立即删除", "next": "下一个", "cameraAudio": "摄像头音频", - "twoWayTalk": "双向对话" + "twoWayTalk": "双向对话", + "continue": "继续" }, "menu": { "system": "系统", @@ -181,7 +199,15 @@ "cs": "捷克语 (Čeština)", "yue": "粤语 (粵語)", "th": "泰语(ไทย)", - "ca": "加泰罗尼亚语 (Català )" + "ca": "加泰罗尼亚语 (Català )", + "ptBR": "巴西葡萄牙语 (Português brasileiro)", + "sr": "塞尔维亚语 (Српски)", + "sl": "斯洛文尼亚语 (Slovenščina)", + "lt": "立陶宛语 (Lietuvių)", + "bg": "保加利亚语 (Български)", + "gl": "加利西亚语 (Galego)", + "id": "印度尼西亚语 (Bahasa Indonesia)", + "ur": "乌尔都语 (اردو)" }, "appearance": "外观", "darkMode": { @@ -229,7 +255,8 @@ "setPassword": "设置密码", "title": "用户" }, - "restart": "重启 Frigate" + "restart": "重启 Frigate", + "classification": "目标分类" }, "toast": { "copyUrlToClipboard": "已复制链接到剪贴板。", @@ -257,5 +284,18 @@ "title": "404", "desc": "页面未找到" }, - "selectItem": "选择 {{item}}" + "selectItem": "选择 {{item}}", + "readTheDocumentation": "阅读文档", + "information": { + "pixels": "{{area}} 像素" + }, + "list": { + "two": "{{0}} 和 {{1}}", + "many": "{{items}} 以及 {{last}}", + "separatorWithSpace": "、 " + }, + "field": { + "optional": "可选", + "internalID": "Frigate 在配置与数据库中使用的内部 ID" + } } diff --git a/web/public/locales/zh-CN/components/auth.json b/web/public/locales/zh-CN/components/auth.json index 015fa0ba8..dbfc34994 100644 --- a/web/public/locales/zh-CN/components/auth.json +++ b/web/public/locales/zh-CN/components/auth.json @@ -10,6 +10,7 @@ "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 index fb1390d46..e01d5e9aa 100644 --- a/web/public/locales/zh-CN/components/camera.json +++ b/web/public/locales/zh-CN/components/camera.json @@ -32,7 +32,7 @@ "title": "{{cameraName}} 视频流设置", "desc": "更改此摄像头组仪表板的实时视频流选项。这些设置特定于设备/浏览器。", "audioIsAvailable": "此视频流支持音频", - "audioIsUnavailable": "此视频流不支持音频", + "audioIsUnavailable": "此视频流不支持音频传输", "audio": { "tips": { "title": "音频必须从您的摄像头输出并在 go2rtc 中配置此流。", @@ -66,7 +66,8 @@ }, "stream": "视频流", "placeholder": "选择视频流" - } + }, + "birdseye": "鸟瞰图" } }, "debug": { @@ -80,7 +81,7 @@ "timestamp": "时间戳", "zones": "区域", "mask": "遮罩", - "motion": "运动", + "motion": "画面变动", "regions": "区域" } } diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json index e7670d1e6..d84e125cf 100644 --- a/web/public/locales/zh-CN/components/dialog.json +++ b/web/public/locales/zh-CN/components/dialog.json @@ -12,7 +12,7 @@ "plus": { "submitToPlus": { "label": "提交至 Frigate+", - "desc": "您希望避开的地点中的物体不应被视为误报。若将其作为误报提交,可能会导致AI模型容易混淆相关物体的识别。" + "desc": "你不希望检测指定地点中的目标或物体不应被视为误报。若将其作为误报提交,可能会导致 AI 模型容易混淆相关目标或物体的识别。" }, "review": { "true": { @@ -28,9 +28,9 @@ }, "question": { "label": "为 Frigate Plus 确认此标签", - "ask_a": "这个对象是 {{label}} 吗?", - "ask_an": "这个对象是 {{label}} 吗?", - "ask_full": "这个对象是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?" + "ask_a": "这个目标/物体是 {{label}} 吗?", + "ask_an": "这个目标/物体是 {{label}} 吗?", + "ask_full": "这个目标/物体是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?" } } }, @@ -59,12 +59,13 @@ "export": "导出", "selectOrExport": "选择或导出", "toast": { - "success": "导出成功。进入 /exports 目录查看文件。", + "success": "导出成功。进入 导出 页面查看文件。", "error": { "failed": "导出失败:{{error}}", "endTimeMustAfterStartTime": "结束时间必须在开始时间之后", "noVaildTimeSelected": "未选择有效的时间范围" - } + }, + "view": "查看" }, "fromTimeline": { "saveExport": "保存导出", @@ -114,7 +115,16 @@ "button": { "export": "导出", "markAsReviewed": "标记为已核查", - "deleteNow": "立即删除" + "deleteNow": "立即删除", + "markAsUnreviewed": "标记为未核查" } + }, + "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 index 5824a421d..9bf90d291 100644 --- a/web/public/locales/zh-CN/components/filter.json +++ b/web/public/locales/zh-CN/components/filter.json @@ -41,15 +41,15 @@ "hasVideoClip": "包含视频片段", "submittedToFrigatePlus": { "label": "提交至 Frigate+", - "tips": "你必须要先筛选具有快照的检测对象。

    没有快照的跟踪对象无法提交至 Frigate+." + "tips": "你必须要先筛选有快照的追踪目标。

    没有快照的追踪目标无法提交至 Frigate+。" } }, "sort": { "label": "排序", "dateAsc": "日期 (正序)", "dateDesc": "日期 (倒序)", - "scoreAsc": "对象分值 (正序)", - "scoreDesc": "对象分值 (倒序)", + "scoreAsc": "目标分值 (正序)", + "scoreDesc": "目标分值 (倒序)", "speedAsc": "预计速度 (正序)", "speedDesc": "预计速度 (倒序)", "relevance": "关联性" @@ -65,14 +65,14 @@ "showReviewed": "显示已核查的项目" }, "motion": { - "showMotionOnly": "仅显示运动" + "showMotionOnly": "仅显示画面变动" }, "explore": { "settings": { "title": "设置", "defaultView": { "title": "默认视图", - "desc": "当未选择任何过滤器时,显示每个标签最近跟踪对象的摘要,或显示未过滤的网格。", + "desc": "当未选择任何筛选条件时,将显示每个标签下最近追踪目标的汇总信息,或者显示未筛选的网格视图。", "summary": "摘要", "unfilteredGrid": "未过滤网格" }, @@ -82,7 +82,7 @@ }, "searchSource": { "label": "搜索源", - "desc": "选择是搜索缩略图还是跟踪对象的描述。", + "desc": "选择是搜索缩略图还是追踪目标的描述。", "options": { "thumbnailImage": "缩略图", "description": "描述" @@ -107,10 +107,10 @@ }, "trackedObjectDelete": { "title": "确认删除", - "desc": "删除这 {{objectLength}} 个跟踪对象将移除快照、任何已保存的嵌入和任何相关的对象生命周期条目。历史视图中这些跟踪对象的录制片段将不会被删除。

    您确定要继续吗?

    按住 Shift 键可在将来跳过此对话框。", + "desc": "删除这 {{objectLength}} 个已追踪目标将移除它们的快照、所有已保存的嵌入向量数据以及任何相关的目标全周期条目,但在 历史 页面中这些追踪目标的录制视频片段将不会被删除。

    您确定要继续吗?

    以后按住 Shift 键进行删除可跳过此提醒。", "toast": { - "success": "跟踪对象删除成功。", - "error": "删除跟踪对象失败:{{errorMessage}}" + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" } }, "zoneMask": { @@ -122,6 +122,20 @@ "loading": "正在加载识别的车牌…", "placeholder": "输入以搜索车牌…", "noLicensePlatesFound": "未找到车牌。", - "selectPlatesFromList": "从列表中选择一个或多个车牌。" + "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/player.json b/web/public/locales/zh-CN/components/player.json index df6648048..0336c32a1 100644 --- a/web/public/locales/zh-CN/components/player.json +++ b/web/public/locales/zh-CN/components/player.json @@ -11,7 +11,7 @@ "title": "视频流离线", "desc": "未在 {{cameraName}} 的 detect 流上接收到任何帧,请检查错误日志" }, - "cameraDisabled": "摄像机已禁用", + "cameraDisabled": "摄像头已禁用", "stats": { "streamType": { "title": "流类型:", diff --git a/web/public/locales/zh-CN/objects.json b/web/public/locales/zh-CN/objects.json index 161821a9d..193f87179 100644 --- a/web/public/locales/zh-CN/objects.json +++ b/web/public/locales/zh-CN/objects.json @@ -71,7 +71,7 @@ "door": "门", "tv": "电视", "laptop": "笔记本电脑", - "mouse": "老鼠", + "mouse": "鼠标", "remote": "遥控器", "keyboard": "键盘", "cell_phone": "手机", 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 000000000..3e9cf67fe --- /dev/null +++ b/web/public/locales/zh-CN/views/classificationModel.json @@ -0,0 +1,183 @@ +{ + "documentTitle": "分类模型 - Frigate", + "button": { + "deleteClassificationAttempts": "删除分类图片", + "renameCategory": "重命名类别", + "deleteCategory": "删除类别", + "deleteImages": "删除图片", + "trainModel": "训练模型", + "addClassification": "添加分类", + "deleteModels": "删除模型", + "editModel": "编辑模型" + }, + "toast": { + "success": { + "deletedCategory": "删除类别", + "deletedImage": "删除图片", + "categorizedImage": "成功分类图片", + "trainedModel": "训练模型成功。", + "trainingModel": "已开始训练模型。", + "deletedModel_other": "已删除 {{count}} 个模型", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改类别名称为 {{name}}" + }, + "error": { + "deleteImageFailed": "删除失败:{{errorMessage}}", + "deleteCategoryFailed": "删除类别失败:{{errorMessage}}", + "categorizeFailed": "图片分类失败:{{errorMessage}}", + "trainingFailed": "训练模型失败,请查看 Frigate 日志获取详情。", + "deleteModelFailed": "删除模型失败:{{errorMessage}}", + "updateModelFailed": "更新模型失败:{{errorMessage}}", + "trainingFailedToStart": "开始训练模型失败:{{errorMessage}}", + "renameCategoryFailed": "修改类别名称失败:{{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": "建议为所有状态都选择示例图片以获得最佳效果。你也可以跳过当前为分类状态选择图片,但需要所有状态都有对应的图片,模型才能够进行训练。跳过后你可通过“最近分类”页面为缺失的状态分类添加图片,然后再训练模型。" + } + } + }, + "deleteModel": { + "title": "删除分类模型", + "single": "你确定要删除 {{name}} 吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。", + "desc": "你确定要删除 {{count}} 个模型吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。" + }, + "menu": { + "objects": "目标", + "states": "状态" + }, + "details": { + "scoreInfo": "得分表示该目标所有检测结果的平均分类置信度。", + "none": "无分类", + "unknown": "未知" + }, + "edit": { + "title": "编辑分类模型", + "descriptionState": "编辑此状态分类模型的类别;更改后需要重新训练模型。", + "descriptionObject": "编辑此目标分类模型的目标类型和分类类型。", + "stateClassesInfo": "注意:更改状态类别后需使用更新后的类别重新训练模型。" + }, + "tooltip": { + "trainingInProgress": "模型正在训练中", + "noNewImages": "没有新的图片可用于训练。请先对数据集中的更多图片进行分类。", + "noChanges": "自上次训练以来,数据集未作任何更改。", + "modelNotReady": "模型尚未准备好进行训练" + }, + "none": "无标签" +} diff --git a/web/public/locales/zh-CN/views/configEditor.json b/web/public/locales/zh-CN/views/configEditor.json index 79e9b398c..a4ca5c5b7 100644 --- a/web/public/locales/zh-CN/views/configEditor.json +++ b/web/public/locales/zh-CN/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "保存配置时出错" } }, - "confirm": "是否退出并不保存?" + "confirm": "是否退出并不保存?", + "safeConfigEditor": "配置编辑器(安全模式)", + "safeModeDescription": "由于验证配置出现错误,Frigate目前为安全模式。" } diff --git a/web/public/locales/zh-CN/views/events.json b/web/public/locales/zh-CN/views/events.json index 72a93104f..9c95ed1c4 100644 --- a/web/public/locales/zh-CN/views/events.json +++ b/web/public/locales/zh-CN/views/events.json @@ -2,14 +2,18 @@ "alerts": "警报", "detections": "检测", "motion": { - "label": "运动", - "only": "仅运动画面" + "label": "画面变动", + "only": "仅变动画面" }, "allCameras": "所有摄像头", "empty": { "alert": "还没有“警报”类核查项", "detection": "还没有“检测”类核查项", - "motion": "还没有运动类数据" + "motion": "还没有画面变动类数据", + "recordingsDisabled": { + "title": "必须要开启录制功能", + "description": "必须要摄像头启用录制功能时,才可为其创建回放项目。" + } }, "timeline": "时间线", "timeline.aria": "选择时间线", @@ -35,5 +39,30 @@ "selected": "已选择 {{count}} 个", "selected_one": "已选择 {{count}} 个", "selected_other": "已选择 {{count}} 个", - "detected": "已检测" + "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/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json index 7db391e4d..8e66f2255 100644 --- a/web/public/locales/zh-CN/views/explore.json +++ b/web/public/locales/zh-CN/views/explore.json @@ -4,14 +4,14 @@ "exploreIsUnavailable": { "title": "浏览功能不可用", "embeddingsReindexing": { - "context": "跟踪对象嵌入重新索引完成后,可以使用浏览功能。", + "context": "完成追踪目标嵌入重新索引后,才可以使用 浏览 功能。", "startingUp": "启动中…", "estimatedTime": "预计剩余时间:", "finishingShortly": "即将完成", "step": { "thumbnailsEmbedded": "缩略图嵌入:", "descriptionsEmbedded": "描述嵌入:", - "trackedObjectsProcessed": "跟踪对象已处理:" + "trackedObjectsProcessed": "追踪目标已处理: " } }, "downloadingModels": { @@ -23,25 +23,27 @@ "textTokenizer": "文本分词器" }, "tips": { - "context": "模型下载完成后,您可能需要重新索引跟踪对象的嵌入。", + "context": "模型下载完成后,您可能需要重新索引追踪目标的嵌入。", "documentation": "阅读文档" }, "error": "发生错误。请检查Frigate日志。" } }, - "trackedObjectDetails": "跟踪对象详情", + "trackedObjectDetails": "目标追踪详情", "type": { "details": "详情", "snapshot": "快照", "video": "视频", - "object_lifecycle": "对象生命周期" + "object_lifecycle": "目标全周期", + "thumbnail": "缩略图", + "tracking_details": "追踪详情" }, "objectLifecycle": { - "title": "对象生命周期", + "title": "目标全周期", "noImageFound": "未找到此时间戳的图像。", - "createObjectMask": "创建对象遮罩", + "createObjectMask": "创建目标/物体遮罩", "adjustAnnotationSettings": "调整标注设置", - "scrollViewTips": "滚动查看此对象生命周期的重要时刻。", + "scrollViewTips": "滚动查看此目标全周期的关键节点。", "autoTrackingTips": "自动跟踪摄像头的边界框位置可能不准确。", "lifecycleItemDesc": { "visible": "检测到 {{label}}", @@ -65,7 +67,7 @@ "title": "标注设置", "showAllZones": { "title": "显示所有区域", - "desc": "在对象进入区域的帧上始终显示区域。" + "desc": "始终在目标进入区域的帧上显示区域标记。" }, "offset": { "label": "标注偏移", @@ -94,19 +96,23 @@ "viewInExplore": "在 浏览 中查看" }, "tips": { - "mismatch_other": "检测到 {{count}} 个不可用的对象,并已包含在此核查项中。这些对象可能未达到警报或检测标准,或者已被清理/删除。", - "hasMissingObjects": "如果希望 Frigate 保存以下标签的跟踪对象,请调整您的配置:{{objects}}" + "mismatch_other": "检测到 {{count}} 个不可用的目标,并已包含在此核查项中。这些目标可能未达到警报或检测标准,或者已被清理/删除。", + "hasMissingObjects": "如果希望 Frigate 保存 {{objects}} 标签的追踪目标,请调整您的配置" }, "toast": { "success": { "regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。", "updatedSublabel": "成功更新子标签。", - "updatedLPR": "成功更新车牌。" + "updatedLPR": "成功更新车牌。", + "audioTranscription": "成功请求音频转录。根据你运行 Frigate 的服务器速度,转录可能需要一些时间才能完成。", + "updatedAttributes": "更新属性成功。" }, "error": { "regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}", "updatedSublabelFailed": "更新子标签失败:{{errorMessage}}", - "updatedLPRFailed": "更新车牌失败:{{errorMessage}}" + "updatedLPRFailed": "更新车牌失败:{{errorMessage}}", + "audioTranscription": "请求音频转录失败:{{errorMessage}}", + "updatedAttributesFailed": "更新属性失败:{{errorMessage}}" } } }, @@ -114,14 +120,14 @@ "editSubLabel": { "title": "编辑子标签", "desc": "为 {{label}} 输入新的子标签", - "descNoLabel": "为此跟踪对象输入新的子标签" + "descNoLabel": "为该追踪目标输入新的子标签" }, "topScore": { "label": "最高得分", - "info": "最高分是跟踪对象的最高中位数得分,因此可能与搜索结果缩略图上显示的得分不同。" + "info": "最高分是追踪目标的中位分数最高值,因此可能与搜索结果缩略图中显示的分数有所不同。" }, "estimatedSpeed": "预计速度", - "objects": "对象", + "objects": "目标/物体", "camera": "摄像头", "zones": "区域", "timestamp": "时间戳", @@ -129,13 +135,13 @@ "findSimilar": "查找相似项", "regenerate": { "title": "重新生成", - "label": "重新生成跟踪对象描述" + "label": "重新生成追踪目标的描述" } }, "description": { "label": "描述", - "placeholder": "跟踪对象的描述", - "aiTips": "在跟踪对象的生命周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。" + "placeholder": "追踪目标的描述", + "aiTips": "在追踪目标的目标全周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。" }, "expandRegenerationMenu": "展开重新生成菜单", "regenerateFromSnapshot": "从快照重新生成", @@ -146,12 +152,23 @@ }, "editLPR": { "desc": "为 {{label}} 输入新的车牌值", - "descNoLabel": "为检测到的对象输入新的车牌值", + "descNoLabel": "为检测到的目标输入新的车牌值", "title": "编辑车牌" }, "recognizedLicensePlate": "识别的车牌", "snapshotScore": { "label": "快照得分" + }, + "score": { + "label": "分值" + }, + "editAttributes": { + "title": "编辑属性", + "desc": "为 {{label}} 选择分类属性" + }, + "attributes": "分类属性", + "title": { + "label": "标题" } }, "itemMenu": { @@ -164,12 +181,12 @@ "aria": "下载快照" }, "viewObjectLifecycle": { - "label": "查看对象生命周期", - "aria": "显示对象的生命周期" + "label": "查看目标全周期", + "aria": "显示目标的全周期" }, "findSimilar": { "label": "查找相似项", - "aria": "查看相似的对象" + "aria": "查看相似的目标/物体" }, "submitToPlus": { "label": "提交至 Frigate+", @@ -180,26 +197,105 @@ "aria": "在历史记录中查看" }, "deleteTrackedObject": { - "label": "删除此跟踪对象" + "label": "删除此追踪目标" + }, + "addTrigger": { + "label": "添加触发器", + "aria": "为该追踪目标添加触发器" + }, + "audioTranscription": { + "label": "转录", + "aria": "请求音频转录" + }, + "showObjectDetails": { + "label": "显示目标轨迹" + }, + "hideObjectDetails": { + "label": "隐藏目标轨迹" + }, + "viewTrackingDetails": { + "label": "查看追踪详情", + "aria": "显示追踪详情" + }, + "downloadCleanSnapshot": { + "label": "下载干净快照", + "aria": "下载干净快照" } }, "dialog": { "confirmDelete": { "title": "确认删除", - "desc": "删除此跟踪对象将移除快照、所有已保存的嵌入数据以及任何关联的对象生命周期条目。但在历史视图中的录制视频不会被删除。

    你确定要继续删除吗?" + "desc": "删除此追踪目标后,将移除快照、所有已保存的嵌入向量数据以及任何相关的目标追踪详情条目,但在 历史 页面中追踪目标的录制视频片段不会被删除。

    你确定要继续删除该追踪目标吗?" } }, - "noTrackedObjects": "未找到跟踪对象", - "fetchingTrackedObjectsFailed": "获取跟踪对象失败:{{errorMessage}}", - "trackedObjectsCount_other": "{{count}} 个跟踪对象 ", + "noTrackedObjects": "未找到追踪目标", + "fetchingTrackedObjectsFailed": "获取追踪目标失败:{{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 个追踪目标 ", "searchResult": { "deleteTrackedObject": { "toast": { - "success": "跟踪对象删除成功。", - "error": "删除跟踪对象失败:{{errorMessage}}" + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" } }, - "tooltip": "与 {{type}} 匹配度为 {{confidence}}%" + "tooltip": "与 {{type}} 匹配度为 {{confidence}}%", + "previousTrackedObject": "上一个追踪目标", + "nextTrackedObject": "下一个追踪目标" }, - "exploreMore": "浏览更多的 {{label}}" + "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/zh-CN/views/exports.json b/web/public/locales/zh-CN/views/exports.json index b22d035f1..3270dc4e5 100644 --- a/web/public/locales/zh-CN/views/exports.json +++ b/web/public/locales/zh-CN/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "重命名导出失败:{{errorMessage}}" } + }, + "tooltip": { + "shareExport": "分享导出", + "downloadVideo": "下载视频", + "editName": "编辑名称", + "deleteExport": "删除导出" } } diff --git a/web/public/locales/zh-CN/views/faceLibrary.json b/web/public/locales/zh-CN/views/faceLibrary.json index 7fb906bcd..0e05f0df7 100644 --- a/web/public/locales/zh-CN/views/faceLibrary.json +++ b/web/public/locales/zh-CN/views/faceLibrary.json @@ -1,14 +1,14 @@ { "description": { - "addFace": "我们将引导你如何向人脸库中添加新的特征库。", - "placeholder": "请输入此特征库的名称", + "addFace": "我们将引导你如何向人脸库中添加新的合集。", + "placeholder": "请输入此合集的名称", "invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。" }, "details": { "person": "人", "confidence": "置信度", "face": "人脸详情", - "faceDesc": "生成此人脸特征的跟踪对象详细信息", + "faceDesc": "生成此人脸特征的追踪目标详细信息", "timestamp": "时间戳", "subLabelScore": "子标签得分", "scoreInfo": "子标签分数是基于所有识别到的人脸置信度的加权评分,因此可能与快照中显示的分数有所不同。", @@ -23,18 +23,19 @@ "title": "创建特征库", "desc": "创建一个新的特征库", "new": "新建人脸", - "nextSteps": "建议按以下步骤建立可靠的特征库:
  • 使用训练选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " + "nextSteps": "建议按以下步骤建立可靠的数据集:
  • 使用近期识别记录选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " }, "train": { - "title": "训练", - "aria": "选择训练", - "empty": "近期未检测到人脸识别操作" + "title": "近期识别记录", + "aria": "选择近期识别记录", + "empty": "近期未检测到人脸识别操作", + "titleShort": "近期" }, "selectItem": "选择 {{item}}", "selectFace": "选择人脸", "deleteFaceLibrary": { "title": "删除名称", - "desc": "确定要删除特征库 {{name}} 吗?此操作将永久删除所有关联的人脸特征数据。" + "desc": "确定要删除数据集 {{name}} 吗?此操作将永久删除所有关联的人脸特征数据。" }, "button": { "deleteFaceAttempts": "删除人脸", @@ -49,7 +50,7 @@ "selectImage": "请选择图片文件。" }, "dropActive": "拖动图片文件到这里…", - "dropInstructions": "拖动图片文件到此处或点击选择", + "dropInstructions": "拖动或粘贴图片文件到此处,也可以点击选择文件", "maxSize": "最大文件大小:{{size}}MB" }, "readTheDocs": "阅读文档", @@ -62,14 +63,14 @@ "deletedFace_other": "成功删除 {{count}} 个 人脸特征。", "deletedName_other": "成功删除 {{count}} 个 人脸特征。", "trainedFace": "人脸特征训练成功。", - "updatedFaceScore": "更新人脸特征评分成功。", + "updatedFaceScore": "更新 {{name}} 人脸特征评分({{score}})成功。", "renamedFace": "成功重命名人脸为{{name}}" }, "error": { "uploadingImageFailed": "图片上传失败:{{errorMessage}}", "addFaceLibraryFailed": "人脸命名失败:{{errorMessage}}", "deleteFaceFailed": "删除失败:{{errorMessage}}", - "deleteNameFailed": "特征集删除失败:{{errorMessage}}", + "deleteNameFailed": "数据集删除失败:{{errorMessage}}", "trainFailed": "训练失败:{{errorMessage}}", "updateFaceScoreFailed": "更新人脸评分失败:{{errorMessage}}", "renameFaceFailed": "重命名人脸失败:{{errorMessage}}" @@ -87,7 +88,7 @@ "desc": "为 {{name}} 输入新的名称", "title": "重命名人脸" }, - "collections": "特征库", + "collections": "合集", "deleteFaceAttempts": { "desc_other": "你确定要删除 {{count}} 张人脸数据吗?此操作不可撤销。", "title": "删除人脸" diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json index 505781c4e..021bafc66 100644 --- a/web/public/locales/zh-CN/views/live.json +++ b/web/public/locales/zh-CN/views/live.json @@ -43,7 +43,15 @@ "label": "点击将PTZ摄像头画面居中" } }, - "presets": "PTZ摄像头预设" + "presets": "PTZ摄像头预设", + "focus": { + "in": { + "label": "PTZ摄像头聚焦" + }, + "out": { + "label": "PTZ摄像头拉远" + } + } }, "camera": { "enable": "开启摄像头", @@ -79,7 +87,7 @@ }, "manualRecording": { "title": "按需录制", - "tips": "根据此摄像头的录制保留设置,手动启动事件。", + "tips": "根据此摄像头的录像存储设置,可以下载即时快照或手动触发事件记录。", "playInBackground": { "label": "后台播放", "desc": "启用此选项可在播放器隐藏时继续视频流播放。" @@ -107,11 +115,11 @@ "title": "视频流", "audio": { "tips": { - "title": "音频必须从摄像头输出并在 go2rtc 中配置为此视频流使用。", + "title": "必须要摄像头能够播放音频,以及需要 go2rtc 支持并配置。", "documentation": "阅读文档 " }, "available": "此视频流支持音频", - "unavailable": "此视频流不支持音频" + "unavailable": "此视频流不支持音频传输" }, "twoWayTalk": { "tips": "您的设备必须支持此功能,并且必须配置 WebRTC 以支持双向对讲。", @@ -126,16 +134,20 @@ "playInBackground": { "label": "后台播放", "tips": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "debug": { + "picker": "调试模式下无法切换视频流。调试将始终使用检测(detect)功能的视频流。" } }, "cameraSettings": { "title": "{{camera}} 设置", "cameraEnabled": "摄像头已启用", - "objectDetection": "对象检测", + "objectDetection": "目标检测", "recording": "录制", "snapshots": "快照", "audioDetection": "音频检测", - "autotracking": "自动跟踪" + "autotracking": "自动追踪", + "transcription": "音频转录" }, "history": { "label": "显示历史录像" @@ -143,16 +155,35 @@ "effectiveRetainMode": { "modes": { "all": "全部", - "motion": "运动", - "active_objects": "活动对象" + "motion": "画面变动", + "active_objects": "活动目标" }, "notAllTips": "您的 {{source}} 录制保留配置设置为 mode: {{effectiveRetainMode}},因此此按需录制将仅保留包含 {{effectiveRetainModeName}} 的片段。" }, "editLayout": { "label": "编辑布局", "group": { - "label": "编辑摄像机分组" + "label": "编辑摄像头分组" }, "exitEdit": "退出编辑" + }, + "transcription": { + "enable": "启用实时音频转录", + "disable": "关闭实时音频转录" + }, + "noCameras": { + "title": "未设置摄像头", + "description": "准备开始连接摄像头至 Frigate 。", + "buttonText": "添加摄像头", + "restricted": { + "title": "无可用摄像头", + "description": "你没有权限查看此分组中的任何摄像头。" + } + }, + "snapshot": { + "takeSnapshot": "下载即时快照", + "noVideoSource": "当前无可用于快照的视频源。", + "captureFailed": "捕获快照失败。", + "downloadStarted": "快照下载已开始。" } } diff --git a/web/public/locales/zh-CN/views/search.json b/web/public/locales/zh-CN/views/search.json index b2f8c6d12..51fe47c8e 100644 --- a/web/public/locales/zh-CN/views/search.json +++ b/web/public/locales/zh-CN/views/search.json @@ -9,10 +9,10 @@ "filterInformation": "筛选信息", "filterActive": "筛选器已激活" }, - "trackedObjectId": "跟踪对象 ID", + "trackedObjectId": "追踪目标 ID", "filter": { "label": { - "cameras": "摄像机", + "cameras": "摄像头", "labels": "标签", "zones": "区域", "sub_labels": "子标签", @@ -26,7 +26,8 @@ "max_speed": "最高速度", "recognized_license_plate": "识别的车牌", "has_clip": "包含片段", - "has_snapshot": "包含快照" + "has_snapshot": "包含快照", + "attributes": "属性" }, "searchType": { "thumbnail": "缩略图", diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index fb92e6b7b..a9a2ee567 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -5,24 +5,30 @@ "camera": "摄像头设置 - Frigate", "classification": "分类设置 - Frigate", "masksAndZones": "遮罩和区域编辑器 - Frigate", - "motionTuner": "运动调整器 - Frigate", + "motionTuner": "画面变动调整 - Frigate", "object": "调试 - Frigate", - "general": "常规设置 - Frigate", + "general": "页面设置 - Frigate", "frigatePlus": "Frigate+ 设置 - Frigate", "notifications": "通知设置 - Frigate", - "enrichments": "增强功能设置 - Frigate" + "enrichments": "增强功能设置 - Frigate", + "cameraManagement": "管理摄像头 - Frigate", + "cameraReview": "摄像头核查设置 - Frigate" }, "menu": { "ui": "界面设置", "classification": "分类设置", "cameras": "摄像头设置", "masksAndZones": "遮罩/ 区域", - "motionTuner": "运动调整器", + "motionTuner": "画面变动调整", "debug": "调试", "users": "用户", "notifications": "通知", "frigateplus": "Frigate+", - "enrichments": "增强功能" + "enrichments": "增强功能", + "triggers": "触发器", + "roles": "权限组", + "cameraManagement": "管理", + "cameraReview": "核查" }, "dialog": { "unsavedChanges": { @@ -35,7 +41,7 @@ "noCamera": "没有摄像头" }, "general": { - "title": "常规设置", + "title": "页面设置", "liveDashboard": { "title": "实时监控面板", "automaticLiveView": { @@ -45,6 +51,14 @@ "playAlertVideos": { "label": "播放警报视频", "desc": "默认情况下,实时监控页面上的最新警报会以一小段循环视频的形式进行播放。禁用此选项将仅显示浏览器本地缓存的静态图片。" + }, + "displayCameraNames": { + "label": "始终显示摄像头名称", + "desc": "在有多摄像头情况下的实时监控页面,将始终显示摄像头名称标签。" + }, + "liveFallbackTimeout": { + "label": "实时监控播放器回退超时", + "desc": "当摄像头的高清实时监控流不可用时,将在此时间后回退到低带宽模式。默认值:3秒。" } }, "storedLayouts": { @@ -164,12 +178,12 @@ "readTheDocumentation": "阅读文档", "noDefinedZones": "该摄像头没有设置区域。", "objectAlertsTips": "所有 {{alertsLabels}} 对象在 {{cameraName}} 下都将显示为警报。", - "zoneObjectAlertsTips": "所有 {{alertsLabels}} 对象在 {{cameraName}} 下的 {{zone}} 区内都将显示为警报。", - "objectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 对象,无论它位于哪个区,都将显示为检测。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 类的目标或物体在 {{cameraName}} 下的 {{zone}} 区内都将显示为警报。", + "objectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区,都将显示为检测。", "zoneObjectDetectionsTips": { - "text": "所有未在 {{cameraName}} 上归类为 {{detectionsLabels}} 的对象在 {{zone}} 区都将显示为检测。", - "notSelectDetections": "所有在 {{cameraName}} 下的 {{zone}} 区内检测到的 {{detectionsLabels}} 对象,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", - "regardlessOfZoneObjectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 对象,无论它位于哪个区域,都将显示为检测。" + "text": "所有未在 {{cameraName}} 上归类为 {{detectionsLabels}} 的目标或物体在 {{zone}} 区内都将显示为检测。", + "notSelectDetections": "所有在 {{cameraName}} 下的 {{zone}} 区内检测到的 {{detectionsLabels}} 目标或物体,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", + "regardlessOfZoneObjectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区域,都将显示为检测。" }, "selectAlertsZones": "选择警报区", "selectDetectionsZones": "选择检测区域", @@ -178,6 +192,44 @@ "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": { @@ -199,7 +251,8 @@ "mustNotBeSameWithCamera": "区域名称不能与摄像头名称相同。", "alreadyExists": "该摄像头已有相同的区域名称。", "mustNotContainPeriod": "区域名称不能包含句点。", - "hasIllegalCharacter": "区域名称包含非法字符。" + "hasIllegalCharacter": "区域名称包含非法字符。", + "mustHaveAtLeastOneLetter": "区域名称必须至少包含一个字母。" } }, "distance": { @@ -246,7 +299,7 @@ "label": "区域", "documentTitle": "编辑区域 - Frigate", "desc": { - "title": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。", + "title": "该功能允许你定义特定区域,以便你可以确定特定目标或物体是否在该区域内。", "documentation": "文档" }, "add": "添加区域", @@ -256,21 +309,21 @@ "name": { "title": "区域名称", "inputPlaceHolder": "请输入名称…", - "tips": "名称至少包含两个字符,且不能和摄像头或其他区域同名。
    当前仅支持英文与数字组合。" + "tips": "名称至少包含两个字符,且不能和摄像头名或该摄像头下的其他区域同名。" }, "inertia": { "title": "惯性", - "desc": "识别指定对象前该对象必须在这个区域内出现了多少帧。默认值:3" + "desc": "识别指定目标前该目标必须在这个区域内出现了多少帧。默认值:3" }, "loiteringTime": { "title": "停留时间", - "desc": "设置对象必须在区域中活动的最小时间(单位为秒)。默认值:0" + "desc": "设置目标必须在区域中至少要活动多少时间(单位为秒)。默认值:0" }, "objects": { - "title": "对象", - "desc": "将在此区域应用的对象列表。" + "title": "目标/物体", + "desc": "将在此区域应用的目标/物体类别列表。" }, - "allObjects": "所有对象", + "allObjects": "所有目标/物体", "speedEstimation": { "title": "速度估算", "desc": "启用此区域内物体的速度估算。该区域必须恰好包含 4 个点。", @@ -291,100 +344,100 @@ } }, "toast": { - "success": "区域 ({{zoneName}}) 已保存。请重启 Frigate 以应用更改。" + "success": "区域 ({{zoneName}}) 已保存。" } }, "motionMasks": { - "label": "运动遮罩", - "documentTitle": "编辑运动遮罩 - Frigate", + "label": "画面变动遮罩", + "documentTitle": "编辑画面变动遮罩 - Frigate", "desc": { - "title": "运动遮罩用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪。", + "title": "画面变动遮罩用于防止触发不必要的画面变动检测。过度的设置遮罩将使目标更加难以被追踪。", "documentation": "文档" }, - "add": "添加运动遮罩", - "edit": "编辑运动遮罩", + "add": "添加画面变动遮罩", + "edit": "编辑画面变动遮罩", "context": { - "title": "运动遮罩用于防止不需要的运动类型触发检测(例如:树枝、摄像头画面显示的时间等)。运动遮罩需要谨慎使用,过度的遮罩会导致追踪对象变得更加困难。", + "title": "画面变动遮罩用于防止不需要的画面变动触发检测(例如:容易被风吹动的树枝、摄像头画面上显示的时间等)。画面变动遮罩应谨慎使用,过度的遮罩会导致追踪目标变得更加困难。", "documentation": "阅读文档" }, "point_other": "{{count}} 点", "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", "polygonAreaTooLarge": { - "title": "运动遮罩的大小达到了摄像头画面的{{polygonArea}}%。不建议设置太大的运动遮罩。", - "tips": "运动遮罩不会阻止检测到对象,你应该使用区域来限制检测对象。", + "title": "画面变动遮罩的大小达到了摄像头画面的{{polygonArea}}%。不建议设置太大的画面变动遮罩。", + "tips": "画面变动遮罩并不会使该区域无法检测到指定目标/物体,如有需要,你应该使用 区域 来限制检测的目标/物体类型。", "documentation": "阅读文档" }, "toast": { "success": { - "title": "{{polygonName}} 已保存。请重启 Frigate 以应用更改。", - "noName": "运动遮罩已保存。请重启 Frigate 以应用更改。" + "title": "{{polygonName}} 已保存。", + "noName": "画面变动遮罩已保存。" } } }, "objectMasks": { - "label": "对象遮罩", - "documentTitle": "编辑对象遮罩 - Frigate", + "label": "目标遮罩", + "documentTitle": "编辑目标遮罩 - Frigate", "desc": { - "title": "对象过滤器用于防止特定位置的指定对象被误报。", + "title": "目标过滤器用于防止特定位置出现对某个目标/物体的误报。", "documentation": "文档" }, - "add": "添加对象遮罩", - "edit": "编辑对象遮罩", - "context": "对象过滤器用于防止特定位置的指定对象被误报。", + "add": "添加目标遮罩", + "edit": "编辑目标遮罩", + "context": "目标过滤器用于防止特定位置的指定目标会误报。", "point_other": "{{count}} 点", "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", "objects": { - "title": "对象", - "desc": "将应用于此对象遮罩的对象类型。", - "allObjectTypes": "所有对象类型" + "title": "目标/物体", + "desc": "将应用于此目标遮罩的目标或物体类型。", + "allObjectTypes": "所有目标或物体类型" }, "toast": { "success": { - "title": "{{polygonName}} 已保存。请重启 Frigate 以应用更改。", - "noName": "对象遮罩已保存。请重启 Frigate 以应用更改。" + "title": "{{polygonName}} 已保存。", + "noName": "目标遮罩已保存。" } } }, "restart_required": "需要重启(遮罩与区域已修改)", - "motionMaskLabel": "运动遮罩 {{number}}", - "objectMaskLabel": "对象遮罩 {{number}}({{label}})" + "motionMaskLabel": "画面变动遮罩 {{number}}", + "objectMaskLabel": "目标/物体遮罩 {{number}}({{label}})" }, "motionDetectionTuner": { - "title": "运动检测调整器", + "title": "画面变动检测调整", "desc": { - "title": "Frigate 将使用运动检测作为首个步骤,以确认一帧画面中是否有对象需要使用对象检测。", - "documentation": "阅读有关运动检测的文档" + "title": "Frigate 将首先使用画面变动检测来确认每一帧画面中是否有变动的区域,然后再对该区域使用目标检测。", + "documentation": "阅读有关画面变动检测的文档" }, "Threshold": { "title": "阈值", - "desc": "阈值决定像素亮度高于多少时会被认为是运动。默认值:30" + "desc": "阈值决定像素亮度变化达到多少时会被认为是画面变动。默认值:30" }, "contourArea": { "title": "轮廓面积", - "desc": "轮廓面积决定哪些变化的像素组符合运动条件。默认值:10" + "desc": "轮廓面积值用于判断产生了多大的变化区域可被认定为画面变动。默认值:10" }, "improveContrast": { "title": "提高对比度", "desc": "提高较暗场景的对比度。默认值:启用" }, "toast": { - "success": "运动设置已保存。" + "success": "画面变动设置已保存。" }, - "unsavedChanges": "{{camera}} 的运动调整器设置未保存" + "unsavedChanges": "{{camera}} 的画面变动调整设置未保存" }, "debug": { "title": "调试", - "detectorDesc": "Frigate 将使用检测器({{detectors}})来检测摄像头视频流中的对象。", - "desc": "调试界面将实时显示被追踪的对象以及统计信息,对象列表将显示检测到的对象和延迟显示的概览。", + "detectorDesc": "Frigate 将使用检测器({{detectors}})来检测摄像头视频流中的目标或物体。", + "desc": "调试界面将实时显示被追踪的目标以及统计信息,目标列表将显示检测到的目标和延迟显示的概览。", "debugging": "调试选项", - "objectList": "对象列表", - "noObjects": "没有对象", + "objectList": "目标列表", + "noObjects": "没有目标", "boundingBoxes": { "title": "边界框", - "desc": "将在被追踪的对象周围显示边界框", + "desc": "将在被追踪的目标周围显示边界框", "colors": { - "label": "对象边界框颜色定义", - "info": "
  • 启用后,将会为每个对象标签分配不同的颜色
  • 深蓝色细线代表该对象在当前时间点未被检测到
  • 灰色细线代表检测到的物体静止不动
  • 粗线表示该对象为自动跟踪的主体(在启动时)
  • " + "label": "目标边界框颜色定义", + "info": "
  • 启用后,将会为每个目标的标签分配不同的颜色
  • 深蓝色细线代表该目标或物体在当前时间点未被检测到
  • 灰色细线代表检测到的目标或物体静止不动
  • 粗线表示在启动自动追踪时,该目标为自动追踪的主体
  • " } }, "timestamp": { @@ -396,28 +449,41 @@ "desc": "显示已定义的区域图层" }, "mask": { - "title": "运动遮罩", - "desc": "显示运动遮罩图层" + "title": "画面变动遮罩", + "desc": "显示画面变动遮罩图层" }, "motion": { - "title": "运动区域框", - "desc": "在检测到运动的区域显示区域框", - "tips": "

    运动区域框


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

    " + "title": "画面变动区域框", + "desc": "在检测到画面变动的区域显示区域框", + "tips": "

    画面变动区域框


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

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

    范围框


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

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

    行动轨迹

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

    " + }, + "audio": { + "title": "音频", + "noAudioDetections": "未检测到音频事件", + "score": "分值", + "currentRMS": "当前均方根值(RMS)", + "currentdbFS": "当前满量程相对分贝值(dbFS)" + }, + "openCameraWebUI": "打开 {{camera}} 的管理页面" }, "users": { "title": "用户", @@ -447,7 +513,7 @@ "role": "权限组", "noUsers": "未找到用户。", "changeRole": "更改用户角色", - "password": "密码", + "password": "修改密码", "deleteUser": "删除用户" }, "dialog": { @@ -472,7 +538,16 @@ "veryStrong": "非常强" }, "match": "密码匹配", - "notMatch": "密码不匹配" + "notMatch": "密码不匹配", + "show": "显示密码", + "hide": "隐藏密码", + "requirements": { + "title": "密码要求:", + "length": "至少8个字符", + "uppercase": "至少一个大写字母", + "digit": "至少一位数字", + "special": "至少一个特殊符号 (!@#$%^&*(),.?\":{}|<>)" + } }, "newPassword": { "title": "新密码", @@ -482,7 +557,11 @@ } }, "usernameIsRequired": "用户名为必填项", - "passwordIsRequired": "必须输入密码" + "passwordIsRequired": "必须输入密码", + "currentPassword": { + "title": "当前密码", + "placeholder": "请输入当前密码" + } }, "createUser": { "title": "创建新用户", @@ -500,7 +579,12 @@ "setPassword": "设置密码", "desc": "创建一个强密码来保护此账户。", "doNotMatch": "两次输入密码不匹配", - "cannotBeEmpty": "密码不能为空" + "cannotBeEmpty": "密码不能为空", + "currentPasswordRequired": "当前密码为必填", + "incorrectCurrentPassword": "当前密码错误", + "passwordVerificationFailed": "验证密码失败", + "multiDeviceWarning": "其他已登录的设备将需要在 {{refresh_time}} 内重新登录。", + "multiDeviceAdmin": "你也可以通过轮换你的 JWT 密钥,强制所有用户立即重新登录验证。" }, "changeRole": { "title": "更改用户权限组", @@ -510,7 +594,8 @@ "viewer": "成员", "viewerDesc": "仅能够查看实时监控面板、核查、浏览和导出功能。", "adminDesc": "完全功能与访问权限。", - "intro": "为该用户选择一个合适的权限组:" + "intro": "为该用户选择一个合适的权限组:", + "customDesc": "自定义特定摄像头的访问规则。" }, "select": "选择权限组" } @@ -619,15 +704,15 @@ "enrichments": { "title": "增强功能设置", "birdClassification": { - "desc": "鸟类分类通过量化的TensorFlow模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。此信息包含在用户界面、筛选器以及通知中。", + "desc": "鸟类分类通过量化的 TensorFlow 模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。此信息包含在用户界面、筛选器以及通知中。", "title": "鸟类分类" }, "semanticSearch": { "reindexNow": { - "desc": "重建索引将为所有跟踪对象重新生成特征向量。该过程将在后台运行,可能会使CPU满载,所需时间取决于跟踪对象的数量。", + "desc": "重建索引将为所有追踪的目标重新生成特征向量信息。该过程将在后台进行,期间可能会使 CPU 满载,所需时间取决于追踪目标的数量。", "label": "立即重建索引", "confirmTitle": "确认重建索引", - "confirmDesc": "确定要为所有跟踪对象重建特征向量索引吗?此过程将在后台运行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", + "confirmDesc": "确定要为所有追踪目标重建特征向量索引信息吗?此过程将在后台进行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", "confirmButton": "重建索引", "success": "重建索引已成功启动。", "alreadyInProgress": "重建索引已在执行中。", @@ -638,19 +723,19 @@ "desc": "用于语义搜索的语言模型大小。", "small": { "title": "小", - "desc": "将使用 模型。该模型将使用少量的内存,在CPU上也能较快的运行,质量较好。" + "desc": "将使用 模型。该模型使用的内存较少,在 CPU 上也能较快的运行,质量较好。" }, "large": { "title": "大", - "desc": "将使用 模型。该选项使用了完整的Jina模型,在合适的时候将自动使用GPU。" + "desc": "将使用 模型。该选项使用了完整的 Jina 模型,条件允许的情况下将自动使用 GPU 运行。" } }, "title": "分类搜索", - "desc": "Frigate中的语义搜索功能允许您通过使用图像本身、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找被追踪对象。", + "desc": "Frigate 中的语义搜索功能将能够让你通过图片、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找目标/物体。", "readTheDocumentation": "阅读文档" }, "licensePlateRecognition": { - "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 recognized_license_plate 字段中,或将已知名称作为子标签添加到汽车类型的对象中。常见的使用场景可能是读取驶入车道的汽车车牌或经过街道的汽车车牌。", + "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 识别的车牌(recognized_license_plate)字段中,或将已知车牌对应的名称作为子标签添加到该车辆目标中。该功能常用于识别驶入车道的车辆车牌或经过街道的车辆车牌。", "title": "车牌识别", "readTheDocumentation": "阅读文档" }, @@ -663,11 +748,11 @@ "desc": "用于人脸识别的模型大小。", "small": { "title": "小", - "desc": "将使用模型。该选项采用FaceNet人脸特征提取模型,可在大多数CPU上高效运行。" + "desc": "将使用模型。该选项采用 FaceNet 人脸特征提取模型,可在大多数 CPU 上高效运行。" }, "large": { "title": "大", - "desc": "将使用模型。该选项使用ArcFace人脸特征提取模型,在需要的时候自动使用GPU运行。" + "desc": "将使用模型。该选项使用 ArcFace 人脸特征提取模型,条件允许的情况下将自动使用 GPU 运行。" } } }, @@ -677,5 +762,545 @@ }, "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": "此操作无法撤销。这将永久删除该权限组,并将所有拥有此角色的用户分配到 “成员” 权限组,该权限组将赋予用户查看所有摄像头的权限。", + "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://”开头;对于非 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当前被锁定为低分辨率。多数海康威视的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。" + } + } + } + }, + "cameraManagement": { + "title": "管理摄像头", + "addCamera": "添加新摄像头", + "editCamera": "编辑摄像头:", + "selectCamera": "选择摄像头", + "backToSettings": "返回摄像头设置", + "streams": { + "title": "开启或关闭摄像头", + "desc": "将临时禁用摄像头,直至 Frigate 重启。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理,届时检测、录制及调试功能均不可用。
    注意:go2rtc 的转流服务不受影响。" + }, + "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}} 已保存" + } + } + }, + "cameraReview": { + "title": "摄像头核查设置", + "object_descriptions": { + "title": "生成式AI目标描述", + "desc": "临时启用或禁用此摄像头的 生成式AI目标描述 功能。禁用后,系统将不再请求该摄像头追踪目标和物体的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式 AI 核查总结", + "desc": "临时开关该摄像头的 生成式 AI 核查总结 功能。禁用后,系统将不再请求 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": "核查分类设置已保存,重启后生效。" + } + } } } diff --git a/web/public/locales/zh-CN/views/system.json b/web/public/locales/zh-CN/views/system.json index befc4bc50..4d06a16bf 100644 --- a/web/public/locales/zh-CN/views/system.json +++ b/web/public/locales/zh-CN/views/system.json @@ -40,16 +40,17 @@ "detector": { "title": "检测器", "inferenceSpeed": "检测器推理速度", - "cpuUsage": "检测器CPU使用率", + "cpuUsage": "检测器 CPU 使用率", "memoryUsage": "检测器内存使用率", - "temperature": "检测器温度" + "temperature": "检测器温度", + "cpuUsageInformation": "此处的 CPU 使用率,只统计在给检测模型准备输入数据和处理输出数据时用到的 CPU。它不统计模型推理本身的资源占用,即使推理是在 GPU 或其他检测器上进行的。" }, "hardwareInfo": { "title": "硬件信息", - "gpuUsage": "GPU使用率", - "gpuMemory": "GPU显存", - "gpuEncoder": "GPU编码", - "gpuDecoder": "GPU解码", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU 显存", + "gpuEncoder": "GPU 编码", + "gpuDecoder": "GPU 解码", "gpuInfo": { "vainfoOutput": { "title": "Vainfo 输出", @@ -65,22 +66,34 @@ "vbios": "VBios信息:{{vbios}}" }, "closeInfo": { - "label": "关闭GPU信息" + "label": "关闭 GPU 信息" }, "copyInfo": { - "label": "复制GPU信息" + "label": "复制 GPU 信息" }, "toast": { - "success": "已复制GPU信息到剪贴板" + "success": "已复制 GPU 信息到剪贴板" } }, "npuMemory": "NPU内存", - "npuUsage": "NPU使用率" + "npuUsage": "NPU 使用率", + "intelGpuWarning": { + "title": "Intel GPU 处于警告状态", + "message": "GPU 状态不可用", + "description": "这是 Intel 的 GPU 状态报告工具(intel_gpu_top)的已知问题:该工具会失效并反复返回 GPU 使用率为 0%,即使在硬件加速和目标检测已在 (i)GPU 上正常运行的情况下也是如此,这并不是 Frigate 的 bug。你可以通过重启主机来临时修复该问题,并确认 GPU 正常工作。该问题并不会影响性能。" + } }, "otherProcesses": { "title": "其他进程", - "processCpuUsage": "主进程CPU使用率", - "processMemoryUsage": "主进程内存使用率" + "processCpuUsage": "主进程 CPU 使用率", + "processMemoryUsage": "主进程内存使用率", + "series": { + "go2rtc": "go2rtc", + "recording": "录制", + "review_segment": "核查片段", + "embeddings": "增强功能", + "audio_detector": "音频检测" + } } }, "storage": { @@ -102,13 +115,17 @@ "title": "未使用", "tips": "如果您的驱动器上存储了除 Frigate 录制内容之外的其他文件,该值可能无法准确反映 Frigate 可用的剩余空间。Frigate 不会追踪录制内容以外的存储使用情况。" } + }, + "shm": { + "title": "共享内存(SHM)分配", + "warning": "当前共享内存(SHM)容量过小( {{total}}MB),请将其至少增加到 {{min_shm}}MB。" } }, "cameras": { "title": "摄像头", "overview": "概览", "info": { - "cameraProbeInfo": "{{camera}} 的摄像头信息", + "cameraProbeInfo": "摄像头 {{camera}} 的信息", "streamDataFromFFPROBE": "流数据信息通过ffprobe获取。", "fetching": "正在获取摄像头数据", "stream": "视频流{{idx}}", @@ -158,7 +175,8 @@ "reindexingEmbeddings": "正在重新索引嵌入(已完成 {{processed}}%)", "detectIsSlow": "{{detect}} 运行缓慢({{speed}}毫秒)", "detectIsVerySlow": "{{detect}} 运行非常缓慢({{speed}}毫秒)", - "cameraIsOffline": "{{camera}} 已离线" + "cameraIsOffline": "{{camera}} 已离线", + "shmTooLow": "/dev/shm 的分配空间过低(当前 {{total}} MB),应至少增加到 {{min}} MB。" }, "enrichments": { "title": "增强功能", @@ -174,7 +192,17 @@ "face_recognition": "人脸特征提取", "plate_recognition": "车牌识别", "yolov9_plate_detection_speed": "YOLOv9 车牌检测速度", - "yolov9_plate_detection": "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 index bb37e6bd4..9a458ce9c 100644 --- a/web/public/locales/zh-Hant/audio.json +++ b/web/public/locales/zh-Hant/audio.json @@ -35,5 +35,47 @@ "vehicle": "車輛", "animal": "動物", "bark": "樹皮", - "goat": "山羊" + "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": "寵物" } diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json index acc7a0a08..17a60efaa 100644 --- a/web/public/locales/zh-Hant/common.json +++ b/web/public/locales/zh-Hant/common.json @@ -39,8 +39,8 @@ "24hour": "M 月 d 日 HH:mm:ss" }, "formattedTimestamp2": { - "12hour": "MM 月 dd 日 ah:mm:ss", - "24hour": "MM 月 dd 日 HH:mm:ss" + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" }, "formattedTimestampHourMinute": { "12hour": "a h:mm", @@ -64,9 +64,12 @@ }, "formattedTimestampMonthDay": "M 月 d 日", "formattedTimestampFilename": { - "12hour": "yy年MM月dd日 ah時mm分ss秒", + "12hour": "yy年MM月dd日 h時mm分ss秒", "24hour": "yy年MM月dd日 HH時mm分ss秒" - } + }, + "inProgress": "處理中", + "invalidStartTime": "無效的起始時間", + "invalidEndTime": "無效的結束時間" }, "unit": { "speed": { @@ -76,10 +79,23 @@ "length": { "feet": "英尺", "meters": "公尺" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" } }, "label": { - "back": "返回" + "back": "返回", + "hide": "隱藏{{item}}", + "show": "顯示{{item}}", + "ID": "ID", + "none": "無", + "all": "全部" }, "button": { "apply": "套用", @@ -89,8 +105,8 @@ "enable": "啟用", "disabled": "已停用", "disable": "停用", - "save": "保存", - "saving": "保存中…", + "save": "儲存", + "saving": "儲存中…", "cancel": "取消", "close": "關閉", "copy": "複製", @@ -116,7 +132,8 @@ "unselect": "取消選取", "export": "匯出", "deleteNow": "立即刪除", - "next": "繼續" + "next": "繼續", + "continue": "繼續" }, "menu": { "system": "系統", @@ -160,7 +177,15 @@ "ca": "Català (加泰隆尼亞文)", "withSystem": { "label": "使用系統語言設定" - } + }, + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)" }, "appearance": "外觀", "darkMode": { @@ -207,7 +232,8 @@ "anonymous": "匿名", "logout": "登出", "setPassword": "設定密碼" - } + }, + "classification": "標籤分類" }, "toast": { "copyUrlToClipboard": "已複製連結至剪貼簿。", @@ -247,5 +273,18 @@ "title": "404", "desc": "找不到頁面" }, - "selectItem": "選擇 {{item}}" + "selectItem": "選擇 {{item}}", + "readTheDocumentation": "閱讀文件", + "list": { + "two": "{{0}}和{{1}}", + "many": "{{items}}和{{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "可選的", + "internalID": "在Frigate 設定檔和資料庫使用的內部ID" + }, + "information": { + "pixels": "{{area}}px" + } } diff --git a/web/public/locales/zh-Hant/components/auth.json b/web/public/locales/zh-Hant/components/auth.json index 34b97ef78..fbc70c4d4 100644 --- a/web/public/locales/zh-Hant/components/auth.json +++ b/web/public/locales/zh-Hant/components/auth.json @@ -10,6 +10,7 @@ "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 index d07662c7e..3bace4d9d 100644 --- a/web/public/locales/zh-Hant/components/camera.json +++ b/web/public/locales/zh-Hant/components/camera.json @@ -66,7 +66,8 @@ "label": "相容模式", "desc": "只有在鏡頭的串流影像中出現色彩異常及右側有斜線時才啟用此選項。" } - } + }, + "birdseye": "鳥瞰" } }, "debug": { diff --git a/web/public/locales/zh-Hant/components/dialog.json b/web/public/locales/zh-Hant/components/dialog.json index a29b487e1..b28ccca48 100644 --- a/web/public/locales/zh-Hant/components/dialog.json +++ b/web/public/locales/zh-Hant/components/dialog.json @@ -51,12 +51,13 @@ "export": "匯出", "selectOrExport": "選擇或匯出", "toast": { - "success": "成功開始匯出。請至 /exports 資料夾查看匯出資料。", + "success": "成功開始匯出。至 /exports 頁查看匯出資料。", "error": { "failed": "匯出失敗:{{error}}", "endTimeMustAfterStartTime": "結束時間必須要在開始時間之後", "noVaildTimeSelected": "沒有選取有效的時間範圍" - } + }, + "view": "查看" }, "fromTimeline": { "saveExport": "保存匯出資料", @@ -80,7 +81,7 @@ }, "search": { "saveSearch": { - "label": "保存搜尋", + "label": "儲存搜尋", "desc": "替此保存的搜尋命名。", "placeholder": "請輸入搜尋的名稱", "overwrite": "{{searchName}} 已存在。保存將會覆蓋現有資料。", @@ -106,7 +107,16 @@ "button": { "export": "匯出", "markAsReviewed": "標記為已審核", - "deleteNow": "立即刪除" + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審核" } + }, + "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 index a1192ac59..1cbef2fd3 100644 --- a/web/public/locales/zh-Hant/components/filter.json +++ b/web/public/locales/zh-Hant/components/filter.json @@ -121,6 +121,20 @@ "loading": "讀取已辨識車牌中…", "placeholder": "輸入以搜尋車牌…", "noLicensePlatesFound": "未找到車牌。", - "selectPlatesFromList": "從列表中選擇一個或多個車牌。" + "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/input.json b/web/public/locales/zh-Hant/components/input.json index df3ed93c0..ed7eee77c 100644 --- a/web/public/locales/zh-Hant/components/input.json +++ b/web/public/locales/zh-Hant/components/input.json @@ -3,7 +3,7 @@ "downloadVideo": { "label": "下載影片", "toast": { - "success": "你的審查影片已開始下載。" + "success": "你的審查項目影片已開始下載。" } } } 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 000000000..32fce2423 --- /dev/null +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -0,0 +1,104 @@ +{ + "toast": { + "success": { + "deletedImage": "已刪除的圖片", + "deletedModel_other": "已成功刪除 {{count}} 個模型", + "deletedCategory": "已刪除分類", + "categorizedImage": "成功分類圖片", + "trainedModel": "訓練模型成功。", + "trainingModel": "已開始訓練模型。", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改分類名稱為{{name}}" + }, + "error": { + "deleteImageFailed": "刪除失敗:{{errorMessage}}", + "deleteCategoryFailed": "刪除分類標籤失敗: {{errorMessage}}", + "deleteModelFailed": "刪除模型失敗: {{errorMessage}}", + "categorizeFailed": "圖片分類失敗: {{errorMessage}}", + "trainingFailed": "模型訓練失敗。請至Frigate 日誌查看詳情。", + "trainingFailedToStart": "模型訓練啟動失敗: {{errorMessage}}", + "updateModelFailed": "模型更新失敗: {{errorMessage}}", + "renameCategoryFailed": "類別重新命名失敗: {{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": "物件" + }, + "steps": { + "chooseExamples": "選擇範本" + } + } +} diff --git a/web/public/locales/zh-Hant/views/configEditor.json b/web/public/locales/zh-Hant/views/configEditor.json index 3788bace0..f1943edbb 100644 --- a/web/public/locales/zh-Hant/views/configEditor.json +++ b/web/public/locales/zh-Hant/views/configEditor.json @@ -12,5 +12,7 @@ } }, "saveOnly": "僅保存", - "confirm": "是否不保存就離開?" + "confirm": "是否不保存就離開?", + "safeConfigEditor": "設定編輯器(安全模式)", + "safeModeDescription": "由於設定驗證有誤,Frigate 進入安全模式。" } diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json index 8f840aab1..c8883f420 100644 --- a/web/public/locales/zh-Hant/views/events.json +++ b/web/public/locales/zh-Hant/views/events.json @@ -8,7 +8,11 @@ "empty": { "motion": "未找到移動資料", "alert": "沒有警告需要審核", - "detection": "沒有偵測到的內容需要審核" + "detection": "沒有偵測到的內容需要審核", + "recordingsDisabled": { + "title": "必須啟用錄製功能", + "description": "僅當該攝影機啟用錄製功能時,才能為該攝影機建立審查項目。" + } }, "timeline": "時間線", "timeline.aria": "選擇時間線", @@ -34,5 +38,29 @@ "selected_one": "已選擇 {{count}} 個", "selected_other": "已選擇 {{count}} 個", "camera": "鏡頭", - "detected": "已偵測" + "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": "安全隱憂" } diff --git a/web/public/locales/zh-Hant/views/explore.json b/web/public/locales/zh-Hant/views/explore.json index 6997b08dd..598700963 100644 --- a/web/public/locales/zh-Hant/views/explore.json +++ b/web/public/locales/zh-Hant/views/explore.json @@ -47,12 +47,16 @@ "success": { "regenerate": "已從 {{provider}} 請求新的說明。根據提供者的速度,生成新的說明可能會需要一段時間。", "updatedSublabel": "成功更新子標籤。", - "updatedLPR": "成功更新車牌。" + "updatedLPR": "成功更新車牌。", + "updatedAttributes": "已成功更新屬性。", + "audioTranscription": "已成功送出音訊轉錄請求。轉錄完成所需時間會依您的 Frigate 伺服器速度而定,可能需要一段時間。" }, "error": { "regenerate": "請求 {{provider}} 生成新的說明失敗:{{errorMessage}}", "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", - "updatedLPRFailed": "更新車牌失敗:{{errorMessage}}" + "updatedLPRFailed": "更新車牌失敗:{{errorMessage}}", + "updatedAttributesFailed": "更新屬性失敗:{{errorMessage}}", + "audioTranscription": "請求音訊轉錄失敗:{{errorMessage}}" } } }, @@ -97,6 +101,17 @@ "tips": { "descriptionSaved": "成功保存說明", "saveDescriptionFailed": "更新說明失敗:{{errorMessage}}" + }, + "editAttributes": { + "title": "編輯屬性", + "desc": "為此 {{label}} 選擇分類屬性" + }, + "score": { + "label": "分數" + }, + "attributes": "分類屬性", + "title": { + "label": "標題" } }, "trackedObjectDetails": "追蹤物件詳情", @@ -104,7 +119,9 @@ "details": "詳情", "snapshot": "截圖", "video": "影片", - "object_lifecycle": "物件生命週期" + "object_lifecycle": "物件生命週期", + "thumbnail": "預覽圖", + "tracking_details": "追蹤詳情" }, "objectLifecycle": { "title": "物件生命週期", @@ -182,12 +199,34 @@ }, "deleteTrackedObject": { "label": "刪除此追蹤物件" + }, + "hideObjectDetails": { + "label": "隱藏物件路徑" + }, + "showObjectDetails": { + "label": "顯示物件路徑" + }, + "addTrigger": { + "label": "新增觸發器", + "aria": "為此追蹤物件新增觸發器" + }, + "audioTranscription": { + "label": "轉錄", + "aria": "請求音訊轉錄" + }, + "downloadCleanSnapshot": { + "label": "下載乾淨的快照", + "aria": "下載乾淨的快照" + }, + "viewTrackingDetails": { + "label": "檢視追蹤詳細資訊", + "aria": "顯示追蹤詳細資訊" } }, "dialog": { "confirmDelete": { "title": "確認刪除", - "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的物件生命週期紀錄。歷史記錄中的錄影不會被刪除。

    你確定要刪除嗎?" + "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會被刪除。

    你確定要刪除嗎?" } }, "noTrackedObjects": "找不到追蹤物件", @@ -200,6 +239,60 @@ "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": "面積" + } + }, + "annotationSettings": { + "title": "標記設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "總是在物件進入區域時在畫面上顯示區域範圍。" + }, + "offset": { + "label": "標記偏移量", + "desc": "這個資料來自您的鏡頭的偵測串流源,但是被疊加在錄影串流源的畫面上,兩個串流源不太可能完美的同步,因此邊框與畫面無法完美的對齊。您可以用這項設定調整標記在時間上前後偏移的補償量來更好的將其與錄影畫面對齊。", + "millisecondsToOffset": "偵測標記偏移補償的毫秒數。預設值: 0", + "tips": "如果影片播放進度超前於方框和路徑點,則降低該值;如果影片播放進度落後於方框和路徑點,則增加該數值。該值可以為負數。", + "toast": { + "success": "{{camera}} 的標記偏移補償量已儲存至設定檔。" + } + } + }, + "carousel": { + "previous": "上一張投影片", + "next": "下一張投影片" } + }, + "aiAnalysis": { + "title": "AI 分析" } } diff --git a/web/public/locales/zh-Hant/views/exports.json b/web/public/locales/zh-Hant/views/exports.json index 159e66e17..3d3f9e87c 100644 --- a/web/public/locales/zh-Hant/views/exports.json +++ b/web/public/locales/zh-Hant/views/exports.json @@ -13,5 +13,11 @@ "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}" } }, - "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?" + "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?", + "tooltip": { + "shareExport": "分享匯出", + "downloadVideo": "下載影片", + "editName": "編輯名稱", + "deleteExport": "刪除匯出" + } } diff --git a/web/public/locales/zh-Hant/views/faceLibrary.json b/web/public/locales/zh-Hant/views/faceLibrary.json index 52675a51b..938bf1581 100644 --- a/web/public/locales/zh-Hant/views/faceLibrary.json +++ b/web/public/locales/zh-Hant/views/faceLibrary.json @@ -1,6 +1,6 @@ { "description": { - "addFace": "了解如何新增圖片集合至人臉資料庫。", + "addFace": "上傳您的第一張照片至臉部資料庫以新增一個新的集合。", "placeholder": "輸入此集合的名稱", "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。" }, @@ -24,7 +24,7 @@ "title": "建立集合", "desc": "建立新集合", "new": "建立新人臉", - "nextSteps": "為了建立可靠的模型基底:
  • 在訓練分頁中選擇並針對每個偵測到人的圖片進行訓練。
  • 請優先使用正臉照以獲得最佳效果,請盡量避免使用從側面或有傾斜角度的人臉
  • " + "nextSteps": "為了建立可靠的模型基底:
  • 在最近的識別紀錄分頁中選擇並針對每個偵測到人的圖片進行訓練。
  • 請優先使用正臉照以獲得最佳效果,請盡量避免使用從側面或有傾斜角度的人臉
  • " }, "steps": { "faceName": "輸入人臉名稱", @@ -35,9 +35,10 @@ } }, "train": { - "title": "訓練", - "aria": "選擇訓練", - "empty": "最近沒有辨識人臉的操作" + "title": "最近的識別紀錄", + "aria": "選擇最近的識別紀錄", + "empty": "最近沒有辨識人臉的操作", + "titleShort": "最近" }, "selectFace": "選擇人臉", "deleteFaceLibrary": { @@ -65,7 +66,7 @@ "selectImage": "請選擇一個圖片檔。" }, "dropActive": "將圖片拖到這裡…", - "dropInstructions": "將圖片拖放至此處,或點擊以選取", + "dropInstructions": "拖放或貼上圖片至此處,或點擊以選取", "maxSize": "最大檔案大小:{{size}}MB" }, "nofaces": "沒有可用的人臉", @@ -81,7 +82,7 @@ "deletedName_other": "{{count}} 個人臉已成功刪除。", "renamedFace": "成功將人臉重新命名為 {{name}}", "trainedFace": "成功訓練人臉。", - "updatedFaceScore": "成功更新人臉分數。" + "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。" }, "error": { "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json index 55947b9f2..a839b4b88 100644 --- a/web/public/locales/zh-Hant/views/live.json +++ b/web/public/locales/zh-Hant/views/live.json @@ -39,7 +39,15 @@ "label": "點擊畫面以置中 PTZ 鏡頭" } }, - "presets": "PTZ 鏡頭預設" + "presets": "PTZ 鏡頭預設", + "focus": { + "in": { + "label": "聚焦 PTZ 鏡頭" + }, + "out": { + "label": "離焦 PTZ 鏡頭" + } + } }, "cameraAudio": { "enable": "啟用鏡頭音訊", @@ -78,8 +86,8 @@ "disable": "隱藏串流統計資料" }, "manualRecording": { - "title": "應需錄影", - "tips": "根據此鏡頭的錄影保留設定手動啟動事件。", + "title": "應需", + "tips": "根據此鏡頭的錄影保留設定,下載快照或手動啟動事件。", "playInBackground": { "label": "背景播放", "desc": "啟用此選項以在播放器被隱藏時繼續播放串流。" @@ -154,5 +162,15 @@ "label": "編輯鏡頭群組" }, "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時語音轉錄", + "disable": "停用即時語音轉錄" + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "沒有可用的影片資源以擷取快照。", + "captureFailed": "快照擷取失敗。", + "downloadStarted": "已開始下載快照。" } } diff --git a/web/public/locales/zh-Hant/views/search.json b/web/public/locales/zh-Hant/views/search.json index 0b56209c2..7fe475e5e 100644 --- a/web/public/locales/zh-Hant/views/search.json +++ b/web/public/locales/zh-Hant/views/search.json @@ -5,7 +5,7 @@ "button": { "clear": "清空搜尋", "filterActive": "過濾中", - "save": "保存搜尋", + "save": "儲存搜尋", "delete": "刪除保存的搜尋", "filterInformation": "過濾資訊" }, @@ -26,7 +26,8 @@ "recognized_license_plate": "已辨識的車牌", "has_clip": "包含片段", "has_snapshot": "包含截圖", - "time_range": "時間範圍" + "time_range": "時間範圍", + "attributes": "屬性" }, "searchType": { "thumbnail": "截圖", diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json index d252250e9..652583a09 100644 --- a/web/public/locales/zh-Hant/views/settings.json +++ b/web/public/locales/zh-Hant/views/settings.json @@ -3,13 +3,15 @@ "default": "設定 - Frigate", "authentication": "認證設定 - Frigate", "camera": "鏡頭設定 - Frigate", - "enrichments": "進階設定 - Frigate", - "general": "一般設定 - Frigate", + "enrichments": "進階功能設定 - Frigate", + "general": "使用者介面設定 - Frigate", "frigatePlus": "Frigate+ 設定 - Frigate", "notifications": "通知設定 - Frigate", "masksAndZones": "遮罩與區域編輯器 - Frigate", "motionTuner": "移動偵測調教器 - Frigate", - "object": "除錯 - Frigate" + "object": "除錯 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "相機預覽設置 - Frigate" }, "menu": { "ui": "使用者介面", @@ -20,7 +22,11 @@ "debug": "除錯", "users": "使用者", "notifications": "通知", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "triggers": "觸發", + "cameraManagement": "管理", + "cameraReview": "預覽", + "roles": "角色" }, "dialog": { "unsavedChanges": { @@ -33,12 +39,107 @@ "noCamera": "沒有鏡頭" }, "general": { - "title": "一般設定", + "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": "小" + } + } + }, + "faceRecognition": { + "title": "人臉識別" + } + }, + "cameraWizard": { + "title": "新增相機", + "testResultLabels": { + "resolution": "解析度", + "video": "影像", + "audio": "語音" + }, + "commonErrors": { + "testFailed": "串流測試失敗: {{error}}" + }, + "step1": { + "description": "輸入相機詳細資訊並選擇自動偵測或手動選擇相機品牌。", + "cameraName": "相機名稱", + "cameraNamePlaceholder": "例: 前門 / 後院", + "host": "主機/IP 位置", + "port": "埠", + "username": "用戶名稱", + "usernamePlaceholder": "選填", + "password": "密碼", + "passwordPlaceholder": "選填", + "selectTransport": "選擇協議", + "cameraBrand": "相機品牌" + } + }, + "triggers": { + "toast": { + "error": { + "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}", + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}" } } } diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json index b3d761047..e956b9a42 100644 --- a/web/public/locales/zh-Hant/views/system.json +++ b/web/public/locales/zh-Hant/views/system.json @@ -2,8 +2,8 @@ "documentTitle": { "cameras": "鏡頭統計 - Frigate", "storage": "儲存裝置統計 - Frigate", - "general": "統計總覽 - Frigate", - "enrichments": "進階統計 - Frigate", + "general": "一般統計 - Frigate", + "enrichments": "進階功能統計 - Frigate", "logs": { "frigate": "Frigate 日誌 - Frigate", "go2rtc": "Go2RTC 日誌 - Frigate", @@ -42,7 +42,8 @@ "inferenceSpeed": "偵測器推理速度", "temperature": "偵測器溫度", "cpuUsage": "偵測器 CPU 使用率", - "memoryUsage": "偵測器記憶體使用量" + "memoryUsage": "偵測器記憶體使用量", + "cpuUsageInformation": "用於準備輸入和輸出數據至/從偵測模型的CPU。此值不衡量推論使用量,即使使用GPU或加速器。" }, "hardwareInfo": { "title": "硬體資訊", @@ -75,12 +76,24 @@ } }, "npuUsage": "NPU 使用率", - "npuMemory": "NPU 記憶體" + "npuMemory": "NPU 記憶體", + "intelGpuWarning": { + "title": "Intel GPU 狀態警告", + "message": "GPU 狀態資訊不可用", + "description": "這是一個在Intel GPU 狀態回報工具 (intel_gpu_top) 中已知的 Bug,該工具會故障並重複的回報 GPU占用率為 0%,甚至在硬體加速與物件偵測在 (i)GPU上正確運作時也是如此。這不是 Frigate 的 Bug。您可以透過重新啟動主機來暫時修復此問題以確認 GPU 運作正常。這不會影響效能。" + } }, "otherProcesses": { "title": "其他行程", "processCpuUsage": "行程 CPU 使用率", - "processMemoryUsage": "行程記憶體使用量" + "processMemoryUsage": "行程記憶體使用量", + "series": { + "recording": "记录", + "review_segment": "评论部分", + "embeddings": "嵌入", + "audio_detector": "音訊偵測器", + "go2rtc": "go2rtc" + } } }, "storage": { @@ -102,6 +115,10 @@ "title": "未使用", "tips": "在磁碟中有除了 Frigate 錄影內容以外的檔案時,此數值可能無法正確反應可用的空間。Frigate 不會追蹤錄影資料以外的檔案的儲存空間用量。" } + }, + "shm": { + "title": "SHM(共享記憶體)配置", + "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。" } }, "cameras": { @@ -158,7 +175,8 @@ "reindexingEmbeddings": "正在重新替嵌入資料建立索引(已完成 {{processed}}%)", "cameraIsOffline": "{{camera}} 已離線", "detectIsSlow": "{{detect}} 偵測速度較慢({{speed}} 毫秒)", - "detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)" + "detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)", + "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。" }, "enrichments": { "title": "進階功能", @@ -174,7 +192,17 @@ "plate_recognition_speed": "車牌辨識速度", "text_embedding_speed": "文字提取速度", "yolov9_plate_detection_speed": "YOLOv9 車牌偵測速度", - "yolov9_plate_detection": "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/notifications-worker.js b/web/public/notifications-worker.js index ab8a6ae44..ba4e033ea 100644 --- a/web/public/notifications-worker.js +++ b/web/public/notifications-worker.js @@ -44,11 +44,16 @@ self.addEventListener("notificationclick", (event) => { switch (event.action ?? "default") { case "markReviewed": if (event.notification.data) { - fetch("/api/reviews/viewed", { - method: "POST", - headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": 1 }, - body: JSON.stringify({ ids: [event.notification.data.id] }), - }); + 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: @@ -58,7 +63,7 @@ self.addEventListener("notificationclick", (event) => { // eslint-disable-next-line no-undef if (clients.openWindow) { // eslint-disable-next-line no-undef - return clients.openWindow(url); + event.waitUntil(clients.openWindow(url)); } } } diff --git a/web/src/App.tsx b/web/src/App.tsx index a0062549f..d7a9ec3e9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,6 +12,10 @@ import { cn } from "./lib/utils"; import { isPWA } from "./utils/isPWA"; import ProtectedRoute from "@/components/auth/ProtectedRoute"; import { AuthProvider } from "@/context/auth-context"; +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")); @@ -22,56 +26,21 @@ 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 Logs = lazy(() => import("@/pages/Logs")); const AccessDenied = lazy(() => import("@/pages/AccessDenied")); function App() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + return ( -
    - {isDesktop && } - {isDesktop && } - {isMobile && } -
    - - - - } - > - } /> - } /> - } /> - } /> - } /> - - } - > - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - - -
    -
    + {config?.safe_mode ? : }
    @@ -79,4 +48,88 @@ function App() { ); } +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/auth-redirect.ts b/web/src/api/auth-redirect.ts new file mode 100644 index 000000000..f19e2b8a5 --- /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/index.tsx b/web/src/api/index.tsx index a9044a6d7..e5c5617ab 100644 --- a/web/src/api/index.tsx +++ b/web/src/api/index.tsx @@ -3,6 +3,7 @@ import { SWRConfig } from "swr"; import { WsProvider } from "./ws"; import axios from "axios"; import { ReactNode } from "react"; +import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect"; axios.defaults.baseURL = `${baseUrl}api/`; @@ -31,7 +32,8 @@ export function ApiProvider({ children, options }: ApiProviderType) { ) { // redirect to the login page if not already there const loginPage = error.response.headers.get("location") ?? "login"; - if (window.location.href !== loginPage) { + if (window.location.href !== loginPage && !isRedirectingToLogin()) { + setRedirectingToLogin(true); window.location.href = loginPage; } } diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 3e9c8c14f..44d45ea2f 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -8,6 +8,9 @@ import { FrigateReview, ModelState, ToggleableSetting, + TrackedObjectUpdateReturnType, + TriggerStatus, + FrigateAudioDetections, } from "@/types/ws"; import { FrigateStats } from "@/types/stats"; import { createContainer } from "react-tracked"; @@ -30,14 +33,9 @@ function useValue(): useValueReturn { // main state - const [hasCameraState, setHasCameraState] = useState(false); const [wsState, setWsState] = useState({}); useEffect(() => { - if (hasCameraState) { - return; - } - const activityValue: string = wsState["camera_activity"] as string; if (!activityValue) { @@ -60,17 +58,23 @@ function useValue(): useValueReturn { enabled, snapshots, audio, + audio_transcription, notifications, notifications_suspended, autotracking, alerts, detections, + object_descriptions, + review_descriptions, } = state["config"]; cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF"; cameraStates[`${name}/enabled/state`] = enabled ? "ON" : "OFF"; cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF"; cameraStates[`${name}/snapshots/state`] = snapshots ? "ON" : "OFF"; cameraStates[`${name}/audio/state`] = audio ? "ON" : "OFF"; + cameraStates[`${name}/audio_transcription/state`] = audio_transcription + ? "ON" + : "OFF"; cameraStates[`${name}/notifications/state`] = notifications ? "ON" : "OFF"; @@ -83,6 +87,12 @@ function useValue(): useValueReturn { cameraStates[`${name}/review_detections/state`] = detections ? "ON" : "OFF"; + cameraStates[`${name}/object_descriptions/state`] = object_descriptions + ? "ON" + : "OFF"; + cameraStates[`${name}/review_descriptions/state`] = review_descriptions + ? "ON" + : "OFF"; }); setWsState((prevState) => ({ @@ -90,12 +100,9 @@ function useValue(): useValueReturn { ...cameraStates, })); - if (Object.keys(cameraStates).length > 0) { - setHasCameraState(true); - } // we only want this to run initially when the config is loaded // eslint-disable-next-line react-hooks/exhaustive-deps - }, [wsState]); + }, [wsState["camera_activity"]]); // ws handler const { sendJsonMessage, readyState } = useWebSocket(wsUrl, { @@ -116,9 +123,7 @@ function useValue(): useValueReturn { retain: false, }); }, - onClose: () => { - setHasCameraState(false); - }, + onClose: () => {}, shouldReconnect: () => true, retryOnError: true, }); @@ -220,6 +225,20 @@ export function useAudioState(camera: string): { 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; @@ -256,6 +275,34 @@ export function useDetectionsState(camera: string): { 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 usePtzCommand(camera: string): { payload: string; send: (payload: string, retain?: boolean) => void; @@ -285,6 +332,13 @@ export function useFrigateEvents(): { payload: FrigateEvent } { return { payload: JSON.parse(payload as string) }; } +export function useAudioDetections(): { payload: FrigateAudioDetections } { + const { + value: { payload }, + } = useWs("audio_detections", ""); + return { payload: JSON.parse(payload as string) }; +} + export function useFrigateReviews(): FrigateReview { const { value: { payload }, @@ -407,6 +461,74 @@ export function useEmbeddingsReindexProgress( return { payload: data }; } +export function useAudioTranscriptionProcessState( + revalidateOnFocus: boolean = true, +): { payload: string } { + const { + value: { payload }, + send: sendCommand, + } = useWs("audio_transcription_state", "audioTranscriptionState"); + + const data = useDeepMemo( + payload ? (JSON.parse(payload as string) as string) : "idle", + ); + + 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 = useDeepMemo(JSON.parse(payload as string)); + + 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 }, @@ -421,6 +543,15 @@ export function useAudioActivity(camera: string): { payload: number } { 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; @@ -463,11 +594,16 @@ export function useImproveContrast(camera: string): { return { payload: payload as ToggleableSetting, send }; } -export function useTrackedObjectUpdate(): { payload: string } { +export function useTrackedObjectUpdate(): { + payload: TrackedObjectUpdateReturnType; +} { const { value: { payload }, } = useWs("tracked_object_update", ""); - return useDeepMemo(JSON.parse(payload as string)); + const parsed = payload + ? JSON.parse(payload as string) + : { type: "", id: "", camera: "" }; + return { payload: useDeepMemo(parsed) }; } export function useNotifications(camera: string): { @@ -505,3 +641,13 @@ export function useNotificationTest(): { } = useWs("notification_test", "notification_test"); return { payload: payload as string, send }; } + +export function useTriggers(): { payload: TriggerStatus } { + const { + value: { payload }, + } = useWs("triggers", ""); + const parsed = payload + ? JSON.parse(payload as string) + : { name: "", camera: "", event_id: "", type: "", score: 0 }; + return { payload: useDeepMemo(parsed) }; +} diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index 0ac6d10a4..ab22a1143 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -116,10 +116,10 @@ export default function Statusbar() { } return ( - + {" "}
    ([]); + 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 = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + 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 index 12e8f777e..8798b5d00 100644 --- a/web/src/components/auth/AuthForm.tsx +++ b/web/src/components/auth/AuthForm.tsx @@ -22,14 +22,24 @@ 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"]); + 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")), @@ -136,6 +146,24 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
    + {showFirstTimeLink && ( + + +

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

    + + {t("readTheDocumentation", { ns: "common" })} + + +
    +
    + )} ); diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx index c35fdaebc..cedf5a15a 100644 --- a/web/src/components/auth/ProtectedRoute.tsx +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -1,15 +1,41 @@ -import { useContext } from "react"; +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"; export default function ProtectedRoute({ requiredRoles, }: { - requiredRoles: ("admin" | "viewer")[]; + 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 = "/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 ( + + ); + } + if (auth.isLoading) { return ( @@ -23,7 +49,9 @@ export default function ProtectedRoute({ // Authenticated mode (8971): require login if (!auth.user) { - return ; + return ( + + ); } // If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback diff --git a/web/src/components/button/BlurredIconButton.tsx b/web/src/components/button/BlurredIconButton.tsx new file mode 100644 index 000000000..8fe17f869 --- /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/camera/DebugCameraImage.tsx b/web/src/components/camera/DebugCameraImage.tsx index 3d840d0d3..924eb86a5 100644 --- a/web/src/components/camera/DebugCameraImage.tsx +++ b/web/src/components/camera/DebugCameraImage.tsx @@ -5,7 +5,7 @@ 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 { usePersistence } from "@/hooks/use-persistence"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage"; import { useTranslation } from "react-i18next"; @@ -24,7 +24,7 @@ export default function DebugCameraImage({ }: DebugCameraImageProps) { const { t } = useTranslation(["components/camera"]); const [showSettings, setShowSettings] = useState(false); - const [options, setOptions] = usePersistence( + const [options, setOptions] = useUserPersistence( `${cameraConfig?.name}-feed`, emptyObject, ); @@ -158,6 +158,16 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) { />
    +
    + { + handleSetOption("paths", isChecked); + }} + /> + +
    ); } diff --git a/web/src/components/camera/FriendlyNameLabel.tsx b/web/src/components/camera/FriendlyNameLabel.tsx new file mode 100644 index 000000000..ca0978852 --- /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/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index d46509eb6..a67dd8305 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -13,7 +13,7 @@ import { baseUrl } from "@/api/baseUrl"; import { VideoPreview } from "../preview/ScrubbablePreview"; import { useApiHost } from "@/api"; import { isDesktop, isSafari } from "react-device-detect"; -import { usePersistence } from "@/hooks/use-persistence"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; import { Skeleton } from "../ui/skeleton"; import { Button } from "../ui/button"; import { FaCircleCheck } from "react-icons/fa6"; @@ -50,6 +50,27 @@ export function AnimatedEventCard({ fetchPreviews: !currentHour, }); + const tooltipText = useMemo(() => { + if (event?.data?.metadata?.title) { + return event.data.metadata.title; + } + + return ( + `${[ + ...new Set([ + ...(event.data.objects || []), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter((item) => item !== undefined && !item.includes("-verified")) + .map((text) => text.charAt(0).toUpperCase() + text.substring(1)) + .sort() + .join(", ") + .replaceAll("-verified", "")} ` + t("detected") + ); + }, [event, t]); + // visibility const [windowVisible, setWindowVisible] = useState(true); @@ -91,7 +112,10 @@ export function AnimatedEventCard({ // image behavior - const [alertVideos] = usePersistence("alertVideos", true); + const [alertVideos, _, alertVideosLoaded] = useUserPersistence( + "alertVideos", + true, + ); const aspectRatio = useMemo(() => { if ( @@ -121,7 +145,7 @@ export function AnimatedEventCard({ + + + {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 000000000..b9943b31a --- /dev/null +++ b/web/src/components/card/EmptyCard.tsx @@ -0,0 +1,49 @@ +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; +}; +export function EmptyCard({ + className, + icon, + title, + titleHeading = true, + description, + buttonText, + link, +}: EmptyCardProps) { + let TitleComponent; + + if (titleHeading) { + TitleComponent = {title}; + } else { + TitleComponent =
    {title}
    ; + } + + return ( +
    + {icon} + {TitleComponent} + {description && ( +
    + {description} +
    + )} + {buttonText?.length && ( + + )} +
    + ); +} diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 9115e0509..021524532 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -4,7 +4,6 @@ import { Button } from "../ui/button"; import { useCallback, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa"; -import Chip from "../indicators/Chip"; import { Skeleton } from "../ui/skeleton"; import { Dialog, @@ -21,6 +20,10 @@ 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 { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { useIsAdmin } from "@/hooks/use-is-admin"; type ExportProps = { className: string; @@ -38,6 +41,7 @@ export default function ExportCard({ onDelete, }: ExportProps) { const { t } = useTranslation(["views/exports"]); + const isAdmin = useIsAdmin(); const [hovered, setHovered] = useState(false); const [loading, setLoading] = useState( exportedRecording.thumb_path.length > 0, @@ -70,7 +74,10 @@ export default function ExportCard({ (editName.update?.length ?? 0) > 0 ) { submitRename(); + return true; } + + return false; }, ); @@ -142,7 +149,7 @@ export default function ExportCard({ <> {exportedRecording.thumb_path.length > 0 ? ( setLoading(false)} /> @@ -152,56 +159,79 @@ export default function ExportCard({ )} {hovered && ( -
    + <>
    -
    - {!exportedRecording.in_progress && ( - - shareOrCopy( - `${baseUrl}export?id=${exportedRecording.id}`, - exportedRecording.name.replaceAll("_", " "), - ) - } - > - - - )} - {!exportedRecording.in_progress && ( - - - - - - )} - {!exportedRecording.in_progress && ( - - setEditName({ - original: exportedRecording.name, - update: undefined, - }) - } - > - - - )} - - onDelete({ - file: exportedRecording.id, - exportName: exportedRecording.name, - }) - } - > - - +
    +
    + {!exportedRecording.in_progress && ( + + + + shareOrCopy( + `${baseUrl}export?id=${exportedRecording.id}`, + exportedRecording.name.replaceAll("_", " "), + ) + } + > + + + + {t("tooltip.shareExport")} + + )} + {!exportedRecording.in_progress && ( + + + + + + + + + {t("tooltip.downloadVideo")} + + + + )} + {isAdmin && !exportedRecording.in_progress && ( + + + + setEditName({ + original: exportedRecording.name, + update: undefined, + }) + } + > + + + + {t("tooltip.editName")} + + )} + {isAdmin && ( + + + + onDelete({ + file: exportedRecording.id, + exportName: exportedRecording.name, + }) + } + > + + + + {t("tooltip.deleteExport")} + + )} +
    {!exportedRecording.in_progress && ( @@ -216,15 +246,14 @@ export default function ExportCard({ )} -
    + )} {loading && ( )} -
    -
    - {exportedRecording.name.replaceAll("_", " ")} -
    + +
    + {exportedRecording.name.replaceAll("_", " ")}
    diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index 09929cec5..b5ba5cfea 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -6,7 +6,7 @@ import { getIconForLabel } from "@/utils/iconUtil"; import { isDesktop, isIOS, isSafari } from "react-device-detect"; import useSWR from "swr"; import TimeAgo from "../dynamic/TimeAgo"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import useImageLoaded from "@/hooks/use-image-loaded"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import { FaCompactDisc } from "react-icons/fa"; @@ -34,17 +34,21 @@ import { toast } from "sonner"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; -import { buttonVariants } from "../ui/button"; +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"; type ReviewCardProps = { event: ReviewSegment; - currentTime: number; + activeReviewItem?: ReviewSegment; onClick?: () => void; }; export default function ReviewCard({ event, - currentTime, + activeReviewItem, onClick, }: ReviewCardProps) { const { t } = useTranslation(["components/dialog"]); @@ -57,12 +61,6 @@ export default function ReviewCard({ : t("time.formattedTimestampHourMinute.12hour", { ns: "common" }), config?.ui.timezone, ); - const isSelected = useMemo( - () => - event.start_time <= currentTime && - (event.end_time ?? Date.now() / 1000) >= currentTime, - [event, currentTime], - ); const [optionsOpen, setOptionsOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -88,6 +86,11 @@ export default function ReviewCard({ if (response.status == 200) { toast.success(t("export.toast.success"), { position: "top-center", + action: ( + + + + ), }); } }) @@ -109,6 +112,7 @@ export default function ReviewCard({ useKeyboardListener(["Shift"], (_, modifiers) => { bypassDialogRef.current = modifiers.shift; + return false; }); const handleDelete = useCallback(() => { @@ -138,7 +142,12 @@ export default function ReviewCard({ /> -
    - <> - {event.data.objects.map((object) => { - return getIconForLabel( - object, - "size-3 text-primary dark:text-white", - ); - })} - {event.data.audio.map((audio) => { - return getIconForLabel( - audio, - "size-3 text-primary dark:text-white", - ); - })} - +
    + +
    + {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}
    @@ -198,6 +219,16 @@ export default function ReviewCard({ dense />
    + {event.data.metadata?.title && ( + +
    + + + {event.data.metadata.title} + +
    +
    + )}
    ); diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index 3876a7710..66f58f4fd 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -133,7 +133,11 @@ export default function SearchThumbnail({ className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`} onClick={() => onClick(searchResult, false, true)} > - {getIconForLabel(objectLabel, "size-3 text-white")} + {getIconForLabel( + objectLabel, + searchResult.data.type, + "size-3 text-white", + )} {Math.floor( (searchResult.data.score ?? searchResult.data.top_score ?? @@ -150,7 +154,9 @@ export default function SearchThumbnail({ .filter( (item) => item !== undefined && !item.includes("-verified"), ) - .map((text) => getTranslatedLabel(text)) + .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 index c86e9c3c6..808ad2831 100644 --- a/web/src/components/card/SearchThumbnailFooter.tsx +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -13,8 +13,8 @@ type SearchThumbnailProps = { columns: number; findSimilar: () => void; refreshResults: () => void; - showObjectLifecycle: () => void; - showSnapshot: () => void; + showTrackingDetails: () => void; + addTrigger: () => void; }; export default function SearchThumbnailFooter({ @@ -22,8 +22,8 @@ export default function SearchThumbnailFooter({ columns, findSimilar, refreshResults, - showObjectLifecycle, - showSnapshot, + showTrackingDetails, + addTrigger, }: SearchThumbnailProps) { const { t } = useTranslation(["views/search"]); const { data: config } = useSWR("config"); @@ -40,11 +40,11 @@ export default function SearchThumbnailFooter({ return (
    4 && "items-start sm:flex-col lg:flex-row lg:items-center", )} > -
    +
    {searchResult.end_time ? ( ) : ( @@ -59,8 +59,8 @@ export default function SearchThumbnailFooter({ searchResult={searchResult} findSimilar={findSimilar} refreshResults={refreshResults} - showObjectLifecycle={showObjectLifecycle} - showSnapshot={showSnapshot} + showTrackingDetails={showTrackingDetails} + addTrigger={addTrigger} />
    diff --git a/web/src/components/classification/ClassificationModelEditDialog.tsx b/web/src/components/classification/ClassificationModelEditDialog.tsx new file mode 100644 index 000000000..a3ff2df8a --- /dev/null +++ b/web/src/components/classification/ClassificationModelEditDialog.tsx @@ -0,0 +1,529 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + CustomClassificationModelConfig, + FrigateConfig, +} from "@/types/frigateConfig"; +import { ClassificationDatasetResponse } from "@/types/classification"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { LuPlus, LuX } from "react-icons/lu"; +import { toast } from "sonner"; +import useSWR, { mutate } from "swr"; +import { z } from "zod"; + +type ClassificationModelEditDialogProps = { + open: boolean; + model: CustomClassificationModelConfig; + onClose: () => void; + onSuccess: () => void; +}; + +type ObjectClassificationType = "sub_label" | "attribute"; + +type ObjectFormData = { + objectLabel: string; + objectType: ObjectClassificationType; +}; + +type StateFormData = { + classes: string[]; +}; + +export default function ClassificationModelEditDialog({ + open, + model, + onClose, + onSuccess, +}: ClassificationModelEditDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const [isSaving, setIsSaving] = useState(false); + + const isStateModel = model.state_config !== undefined; + const isObjectModel = model.object_config !== undefined; + + const objectLabels = useMemo(() => { + if (!config) return []; + + const labels = new Set(); + + Object.values(config.cameras).forEach((cameraConfig) => { + if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) { + return; + } + + cameraConfig.objects.track.forEach((label) => { + if (!config.model.all_attributes.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels].sort(); + }, [config]); + + // Define form schema based on model type + const formSchema = useMemo(() => { + if (isObjectModel) { + return z.object({ + objectLabel: z + .string() + .min(1, t("wizard.step1.errors.objectLabelRequired")), + objectType: z.enum(["sub_label", "attribute"]), + }); + } else { + // State model + return z.object({ + classes: z + .array(z.string()) + .min(1, t("wizard.step1.errors.classRequired")) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 2; + }, + { message: t("wizard.step1.errors.stateRequiresTwoClasses") }, + ) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + const unique = new Set(nonEmpty.map((c) => c.toLowerCase())); + return unique.size === nonEmpty.length; + }, + { message: t("wizard.step1.errors.classesUnique") }, + ), + }); + } + }, [isObjectModel, t]); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: isObjectModel + ? ({ + objectLabel: model.object_config?.objects?.[0] || "", + objectType: + (model.object_config + ?.classification_type as ObjectClassificationType) || "sub_label", + } as ObjectFormData) + : ({ + classes: [""], // Will be populated from dataset + } as StateFormData), + mode: "onChange", + }); + + // Fetch dataset to get current classes for state models + const { data: dataset } = useSWR( + isStateModel ? `classification/${model.name}/dataset` : null, + { + revalidateOnFocus: false, + }, + ); + + // Update form with classes from dataset when loaded + useEffect(() => { + if (isStateModel && dataset?.categories) { + const classes = Object.keys(dataset.categories).filter( + (key) => key !== "none", + ); + if (classes.length > 0) { + (form as ReturnType>).setValue( + "classes", + classes, + ); + } + } + }, [dataset, isStateModel, form]); + + const watchedClasses = isStateModel + ? (form as ReturnType>).watch("classes") + : undefined; + const watchedObjectType = isObjectModel + ? (form as ReturnType>).watch("objectType") + : undefined; + + const handleAddClass = useCallback(() => { + const currentClasses = ( + form as ReturnType> + ).getValues("classes"); + (form as ReturnType>).setValue( + "classes", + [...currentClasses, ""], + { + shouldValidate: true, + }, + ); + }, [form]); + + const handleRemoveClass = useCallback( + (index: number) => { + const currentClasses = ( + form as ReturnType> + ).getValues("classes"); + const newClasses = currentClasses.filter((_, i) => i !== index); + + // Ensure at least one field remains (even if empty) + if (newClasses.length === 0) { + (form as ReturnType>).setValue( + "classes", + [""], + { shouldValidate: true }, + ); + } else { + (form as ReturnType>).setValue( + "classes", + newClasses, + { shouldValidate: true }, + ); + } + }, + [form], + ); + + const onSubmit = useCallback( + async (data: ObjectFormData | StateFormData) => { + setIsSaving(true); + try { + if (isObjectModel) { + const objectData = data as ObjectFormData; + + // Update the config + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${model.name}`, + config_data: { + classification: { + custom: { + [model.name]: { + enabled: model.enabled, + name: model.name, + threshold: model.threshold, + object_config: { + objects: [objectData.objectLabel], + classification_type: objectData.objectType, + }, + }, + }, + }, + }, + }); + + toast.success(t("toast.success.updatedModel"), { + position: "top-center", + }); + } else { + const stateData = data as StateFormData; + const newClasses = stateData.classes.filter( + (c) => c.trim().length > 0, + ); + const oldClasses = dataset?.categories + ? Object.keys(dataset.categories).filter((key) => key !== "none") + : []; + + const renameMap = new Map(); + const maxLength = Math.max(oldClasses.length, newClasses.length); + + for (let i = 0; i < maxLength; i++) { + const oldClass = oldClasses[i]; + const newClass = newClasses[i]; + + if (oldClass && newClass && oldClass !== newClass) { + renameMap.set(oldClass, newClass); + } + } + + const renamePromises = Array.from(renameMap.entries()).map( + async ([oldName, newName]) => { + try { + await axios.put( + `/classification/${model.name}/dataset/${oldName}/rename`, + { + new_category: newName, + }, + ); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + }; + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + throw new Error( + `Failed to rename ${oldName} to ${newName}: ${errorMessage}`, + ); + } + }, + ); + + if (renamePromises.length > 0) { + await Promise.all(renamePromises); + await mutate(`classification/${model.name}/dataset`); + toast.success(t("toast.success.updatedModel"), { + position: "top-center", + }); + } else { + toast.info(t("edit.stateClassesInfo"), { + position: "top-center", + }); + } + } + + onSuccess(); + onClose(); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + error.message || + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.updateModelFailed", { errorMessage }), { + position: "top-center", + }); + } finally { + setIsSaving(false); + } + }, + [isObjectModel, model, dataset, t, onSuccess, onClose], + ); + + const handleCancel = useCallback(() => { + form.reset(); + onClose(); + }, [form, onClose]); + + return ( + !open && handleCancel()}> + + + {t("edit.title")} + + {isStateModel + ? t("edit.descriptionState") + : t("edit.descriptionObject")} + + + +
    +
    + + {isObjectModel && ( + <> + ( + + + {t("wizard.step1.objectLabel")} + + + + + )} + /> + + ( + + + {t("wizard.step1.classificationType")} + + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + )} + + {isStateModel && ( +
    +
    + + {t("wizard.step1.states")} + + +
    +
    + {watchedClasses?.map((_: string, index: number) => ( + >) + .control + } + name={`classes.${index}` as const} + render={({ field }) => ( + + +
    + + {watchedClasses && + watchedClasses.length > 1 && ( + + )} +
    +
    +
    + )} + /> + ))} +
    + {isStateModel && + "classes" in form.formState.errors && + form.formState.errors.classes && ( +

    + {form.formState.errors.classes.message} +

    + )} +
    + )} + +
    + + +
    + + +
    +
    +
    + ); +} diff --git a/web/src/components/classification/ClassificationModelWizardDialog.tsx b/web/src/components/classification/ClassificationModelWizardDialog.tsx new file mode 100644 index 000000000..0c43b9942 --- /dev/null +++ b/web/src/components/classification/ClassificationModelWizardDialog.tsx @@ -0,0 +1,223 @@ +import { useTranslation } from "react-i18next"; +import StepIndicator from "../indicators/StepIndicator"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { useReducer, useMemo } from "react"; +import Step1NameAndDefine, { Step1FormData } from "./wizard/Step1NameAndDefine"; +import Step2StateArea, { Step2FormData } from "./wizard/Step2StateArea"; +import Step3ChooseExamples, { + Step3FormData, +} from "./wizard/Step3ChooseExamples"; +import { cn } from "@/lib/utils"; +import { isDesktop } from "react-device-detect"; +import axios from "axios"; + +const OBJECT_STEPS = [ + "wizard.steps.nameAndDefine", + "wizard.steps.chooseExamples", +]; + +const STATE_STEPS = [ + "wizard.steps.nameAndDefine", + "wizard.steps.stateArea", + "wizard.steps.chooseExamples", +]; + +type ClassificationModelWizardDialogProps = { + open: boolean; + onClose: () => void; + defaultModelType?: "state" | "object"; +}; + +type WizardState = { + currentStep: number; + step1Data?: Step1FormData; + step2Data?: Step2FormData; + step3Data?: Step3FormData; +}; + +type WizardAction = + | { type: "NEXT_STEP"; payload?: Partial } + | { type: "PREVIOUS_STEP" } + | { type: "SET_STEP_1"; payload: Step1FormData } + | { type: "SET_STEP_2"; payload: Step2FormData } + | { type: "SET_STEP_3"; payload: Step3FormData } + | { type: "RESET" }; + +const initialState: WizardState = { + currentStep: 0, +}; + +function wizardReducer(state: WizardState, action: WizardAction): WizardState { + switch (action.type) { + case "SET_STEP_1": + return { + ...state, + step1Data: action.payload, + currentStep: 1, + }; + case "SET_STEP_2": + return { + ...state, + step2Data: action.payload, + currentStep: 2, + }; + case "SET_STEP_3": + return { + ...state, + step3Data: action.payload, + currentStep: 3, + }; + case "NEXT_STEP": + return { + ...state, + ...action.payload, + currentStep: state.currentStep + 1, + }; + case "PREVIOUS_STEP": + return { + ...state, + currentStep: Math.max(0, state.currentStep - 1), + }; + case "RESET": + return initialState; + default: + return state; + } +} + +export default function ClassificationModelWizardDialog({ + open, + onClose, + defaultModelType, +}: ClassificationModelWizardDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + + const [wizardState, dispatch] = useReducer(wizardReducer, initialState); + + const steps = useMemo(() => { + if (!wizardState.step1Data) { + return OBJECT_STEPS; + } + return wizardState.step1Data.modelType === "state" + ? STATE_STEPS + : OBJECT_STEPS; + }, [wizardState.step1Data]); + + const handleStep1Next = (data: Step1FormData) => { + dispatch({ type: "SET_STEP_1", payload: data }); + }; + + const handleStep2Next = (data: Step2FormData) => { + dispatch({ type: "SET_STEP_2", payload: data }); + }; + + const handleBack = () => { + dispatch({ type: "PREVIOUS_STEP" }); + }; + + const handleCancel = async () => { + // Clean up any generated training images if we're cancelling from Step 3 + if (wizardState.step1Data && wizardState.step3Data?.examplesGenerated) { + try { + await axios.delete( + `/classification/${wizardState.step1Data.modelName}`, + ); + } catch (error) { + // Silently fail - user is already cancelling + } + } + + dispatch({ type: "RESET" }); + onClose(); + }; + + const handleSuccessClose = () => { + dispatch({ type: "RESET" }); + onClose(); + }; + + return ( + { + if (!open) { + handleCancel(); + } + }} + > + 0 && + "max-h-[90%] max-w-[70%] overflow-y-auto xl:max-h-[80%]", + )} + onInteractOutside={(e) => { + e.preventDefault(); + }} + > + + + {t("wizard.title")} + {wizardState.currentStep === 0 && ( + + {t("wizard.step1.description")} + + )} + {wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "state" && ( + + {t("wizard.step2.description")} + + )} + + +
    + {wizardState.currentStep === 0 && ( + + )} + {wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "state" && ( + + )} + {((wizardState.currentStep === 2 && + wizardState.step1Data?.modelType === "state") || + (wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "object")) && + wizardState.step1Data && ( + + )} +
    +
    +
    + ); +} diff --git a/web/src/components/classification/wizard/Step1NameAndDefine.tsx b/web/src/components/classification/wizard/Step1NameAndDefine.tsx new file mode 100644 index 000000000..a4cdc4867 --- /dev/null +++ b/web/src/components/classification/wizard/Step1NameAndDefine.tsx @@ -0,0 +1,508 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useMemo } from "react"; +import { LuX, LuPlus, LuInfo, LuExternalLink } from "react-icons/lu"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export type ModelType = "state" | "object"; +export type ObjectClassificationType = "sub_label" | "attribute"; + +export type Step1FormData = { + modelName: string; + modelType: ModelType; + objectLabel?: string; + objectType?: ObjectClassificationType; + classes: string[]; +}; + +type Step1NameAndDefineProps = { + initialData?: Partial; + defaultModelType?: "state" | "object"; + onNext: (data: Step1FormData) => void; + onCancel: () => void; +}; + +export default function Step1NameAndDefine({ + initialData, + defaultModelType, + onNext, + onCancel, +}: Step1NameAndDefineProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const { getLocaleDocUrl } = useDocDomain(); + + const objectLabels = useMemo(() => { + if (!config) return []; + + const labels = new Set(); + + Object.values(config.cameras).forEach((cameraConfig) => { + if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) { + return; + } + + cameraConfig.objects.track.forEach((label) => { + if (!config.model.all_attributes.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels].sort(); + }, [config]); + + const step1FormData = z + .object({ + modelName: z + .string() + .min(1, t("wizard.step1.errors.nameRequired")) + .max(64, t("wizard.step1.errors.nameLength")) + .refine((value) => !/^\d+$/.test(value), { + message: t("wizard.step1.errors.nameOnlyNumbers"), + }), + modelType: z.enum(["state", "object"]), + objectLabel: z.string().optional(), + objectType: z.enum(["sub_label", "attribute"]).optional(), + classes: z + .array( + z + .string() + .refine( + (val) => val.trim().toLowerCase() !== "none", + t("wizard.step1.errors.noneNotAllowed"), + ), + ) + .min(1, t("wizard.step1.errors.classRequired")) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 1; + }, + { message: t("wizard.step1.errors.classRequired") }, + ) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + const unique = new Set(nonEmpty.map((c) => c.toLowerCase())); + return unique.size === nonEmpty.length; + }, + { message: t("wizard.step1.errors.classesUnique") }, + ), + }) + .refine( + (data) => { + // State models require at least 2 classes + if (data.modelType === "state") { + const nonEmpty = data.classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 2; + } + return true; + }, + { + message: t("wizard.step1.errors.stateRequiresTwoClasses"), + path: ["classes"], + }, + ) + .refine( + (data) => { + if (data.modelType === "object") { + return data.objectLabel !== undefined && data.objectLabel !== ""; + } + return true; + }, + { + message: t("wizard.step1.errors.objectLabelRequired"), + path: ["objectLabel"], + }, + ) + .refine( + (data) => { + if (data.modelType === "object") { + return data.objectType !== undefined; + } + return true; + }, + { + message: t("wizard.step1.errors.objectTypeRequired"), + path: ["objectType"], + }, + ); + + const form = useForm>({ + resolver: zodResolver(step1FormData), + defaultValues: { + modelName: initialData?.modelName || "", + modelType: initialData?.modelType || defaultModelType || "state", + objectLabel: initialData?.objectLabel, + objectType: initialData?.objectType || "sub_label", + classes: initialData?.classes?.length ? initialData.classes : [""], + }, + mode: "onChange", + }); + + const watchedClasses = form.watch("classes"); + const watchedModelType = form.watch("modelType"); + const watchedObjectType = form.watch("objectType"); + + const handleAddClass = () => { + const currentClasses = form.getValues("classes"); + form.setValue("classes", [...currentClasses, ""], { shouldValidate: true }); + }; + + const handleRemoveClass = (index: number) => { + const currentClasses = form.getValues("classes"); + const newClasses = currentClasses.filter((_, i) => i !== index); + + // Ensure at least one field remains (even if empty) + if (newClasses.length === 0) { + form.setValue("classes", [""], { shouldValidate: true }); + } else { + form.setValue("classes", newClasses, { shouldValidate: true }); + } + }; + + const onSubmit = (data: z.infer) => { + // Filter out empty classes + const filteredClasses = data.classes.filter((c) => c.trim().length > 0); + onNext({ + ...data, + classes: filteredClasses, + }); + }; + + return ( +
    +
    + + ( + + + {t("wizard.step1.name")} + + + + + + + )} + /> + + ( + + + {t("wizard.step1.type")} + + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + {watchedModelType === "object" && ( + <> + ( + + + {t("wizard.step1.objectLabel")} + + + + + )} + /> + + ( + +
    + + {t("wizard.step1.classificationType")} + + + + + + +
    +
    + {t("wizard.step1.classificationTypeDesc")} +
    + +
    +
    +
    +
    + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + )} + +
    +
    +
    + + {watchedModelType === "state" + ? t("wizard.step1.states") + : t("wizard.step1.classes")} + + + + + + +
    +
    + {watchedModelType === "state" + ? t("wizard.step1.classesStateDesc") + : t("wizard.step1.classesObjectDesc")} +
    + +
    +
    +
    +
    + +
    +
    + {watchedClasses.map((_, index) => ( + ( + + +
    + + {watchedClasses.length > 1 && ( + + )} +
    +
    + +
    + )} + /> + ))} +
    + {form.formState.errors.classes && ( +

    + {form.formState.errors.classes.message} +

    + )} +
    + + + +
    + + +
    +
    + ); +} diff --git a/web/src/components/classification/wizard/Step2StateArea.tsx b/web/src/components/classification/wizard/Step2StateArea.tsx new file mode 100644 index 000000000..38c2fcad7 --- /dev/null +++ b/web/src/components/classification/wizard/Step2StateArea.tsx @@ -0,0 +1,479 @@ +import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; +import { useState, useMemo, useRef, useCallback, useEffect } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { LuX, LuPlus } from "react-icons/lu"; +import { Stage, Layer, Rect, Transformer } from "react-konva"; +import Konva from "konva"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { useApiHost } from "@/api"; +import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; +import Heading from "@/components/ui/heading"; +import { isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; + +export type CameraAreaConfig = { + camera: string; + crop: [number, number, number, number]; +}; + +export type Step2FormData = { + cameraAreas: CameraAreaConfig[]; +}; + +type Step2StateAreaProps = { + initialData?: Partial; + onNext: (data: Step2FormData) => void; + onBack: () => void; +}; + +export default function Step2StateArea({ + initialData, + onNext, + onBack, +}: Step2StateAreaProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + + const [cameraAreas, setCameraAreas] = useState( + initialData?.cameraAreas || [], + ); + const [selectedCameraIndex, setSelectedCameraIndex] = useState(0); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); + + const containerRef = useRef(null); + const imageRef = useRef(null); + const stageRef = useRef(null); + const rectRef = useRef(null); + const transformerRef = useRef(null); + + const [{ width: containerWidth }] = useResizeObserver(containerRef); + + const availableCameras = useMemo(() => { + if (!config) return []; + + const selectedCameraNames = cameraAreas.map((ca) => ca.camera); + return Object.entries(config.cameras) + .sort() + .filter( + ([name, cam]) => + cam.enabled && + cam.enabled_in_config && + !selectedCameraNames.includes(name), + ) + .map(([name]) => ({ + name, + displayName: resolveCameraName(config, name), + })); + }, [config, cameraAreas]); + + const selectedCamera = useMemo(() => { + if (cameraAreas.length === 0) return null; + return cameraAreas[selectedCameraIndex]; + }, [cameraAreas, selectedCameraIndex]); + + const selectedCameraConfig = useMemo(() => { + if (!config || !selectedCamera) return null; + return config.cameras[selectedCamera.camera]; + }, [config, selectedCamera]); + + const imageSize = useMemo(() => { + if (!containerWidth || !selectedCameraConfig) { + return { width: 0, height: 0 }; + } + + const containerAspectRatio = 16 / 9; + const containerHeight = containerWidth / containerAspectRatio; + + const cameraAspectRatio = + selectedCameraConfig.detect.width / selectedCameraConfig.detect.height; + + // Fit camera within 16:9 container + let imageWidth, imageHeight; + if (cameraAspectRatio > containerAspectRatio) { + imageWidth = containerWidth; + imageHeight = imageWidth / cameraAspectRatio; + } else { + imageHeight = containerHeight; + imageWidth = imageHeight * cameraAspectRatio; + } + + return { width: imageWidth, height: imageHeight }; + }, [containerWidth, selectedCameraConfig]); + + const handleAddCamera = useCallback( + (cameraName: string) => { + // Calculate a square crop in pixel space + const camera = config?.cameras[cameraName]; + if (!camera) return; + + const cameraAspect = camera.detect.width / camera.detect.height; + const cropSize = 0.3; + let x1, y1, x2, y2; + + if (cameraAspect >= 1) { + const pixelSize = cropSize * camera.detect.height; + const normalizedWidth = pixelSize / camera.detect.width; + x1 = (1 - normalizedWidth) / 2; + y1 = (1 - cropSize) / 2; + x2 = x1 + normalizedWidth; + y2 = y1 + cropSize; + } else { + const pixelSize = cropSize * camera.detect.width; + const normalizedHeight = pixelSize / camera.detect.height; + x1 = (1 - cropSize) / 2; + y1 = (1 - normalizedHeight) / 2; + x2 = x1 + cropSize; + y2 = y1 + normalizedHeight; + } + + const newArea: CameraAreaConfig = { + camera: cameraName, + crop: [x1, y1, x2, y2], + }; + setCameraAreas([...cameraAreas, newArea]); + setSelectedCameraIndex(cameraAreas.length); + setIsPopoverOpen(false); + }, + [cameraAreas, config], + ); + + const handleRemoveCamera = useCallback( + (index: number) => { + const newAreas = cameraAreas.filter((_, i) => i !== index); + setCameraAreas(newAreas); + if (selectedCameraIndex >= newAreas.length) { + setSelectedCameraIndex(Math.max(0, newAreas.length - 1)); + } + }, + [cameraAreas, selectedCameraIndex], + ); + + const handleCropChange = useCallback( + (crop: [number, number, number, number]) => { + const newAreas = [...cameraAreas]; + newAreas[selectedCameraIndex] = { + ...newAreas[selectedCameraIndex], + crop, + }; + setCameraAreas(newAreas); + }, + [cameraAreas, selectedCameraIndex], + ); + + useEffect(() => { + setImageLoaded(false); + }, [selectedCamera]); + + useEffect(() => { + const rect = rectRef.current; + const transformer = transformerRef.current; + + if ( + rect && + transformer && + selectedCamera && + imageSize.width > 0 && + imageLoaded + ) { + rect.scaleX(1); + rect.scaleY(1); + transformer.nodes([rect]); + transformer.getLayer()?.batchDraw(); + } + }, [selectedCamera, imageSize, imageLoaded]); + + const handleRectChange = useCallback(() => { + const rect = rectRef.current; + + if (rect && imageSize.width > 0) { + const actualWidth = rect.width() * rect.scaleX(); + const actualHeight = rect.height() * rect.scaleY(); + + // Average dimensions to maintain perfect square + const size = (actualWidth + actualHeight) / 2; + + rect.width(size); + rect.height(size); + rect.scaleX(1); + rect.scaleY(1); + + const x1 = rect.x() / imageSize.width; + const y1 = rect.y() / imageSize.height; + const x2 = (rect.x() + size) / imageSize.width; + const y2 = (rect.y() + size) / imageSize.height; + + handleCropChange([x1, y1, x2, y2]); + } + }, [imageSize, handleCropChange]); + + const handleContinue = useCallback(() => { + onNext({ cameraAreas }); + }, [cameraAreas, onNext]); + + const canContinue = cameraAreas.length > 0; + + return ( +
    +
    +
    +
    +

    {t("wizard.step2.cameras")}

    + {availableCameras.length > 0 ? ( + + + + + e.preventDefault()} + > +
    + + {t("wizard.step2.selectCamera")} + +
    + {availableCameras.map((cam) => ( + + ))} +
    +
    +
    +
    + ) : ( + + )} +
    + +
    + {cameraAreas.map((area, index) => { + const isSelected = index === selectedCameraIndex; + const displayName = resolveCameraName(config, area.camera); + + return ( +
    setSelectedCameraIndex(index)} + > + {displayName} + +
    + ); + })} +
    + + {cameraAreas.length === 0 && ( +
    + {t("wizard.step2.noCameras")} +
    + )} +
    + +
    +
    + {selectedCamera && selectedCameraConfig && imageSize.width > 0 ? ( +
    + {resolveCameraName(config, setImageLoaded(true)} + /> + + + { + const rect = rectRef.current; + if (!rect) return pos; + + const size = rect.width(); + const x = Math.max( + 0, + Math.min(pos.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(pos.y, imageSize.height - size), + ); + + return { x, y }; + }} + onDragEnd={handleRectChange} + onTransformEnd={handleRectChange} + /> + { + const minSize = 50; + const maxSize = Math.min( + imageSize.width, + imageSize.height, + ); + + // Clamp dimensions to stage bounds first + const clampedWidth = Math.max( + minSize, + Math.min(newBox.width, maxSize), + ); + const clampedHeight = Math.max( + minSize, + Math.min(newBox.height, maxSize), + ); + + // Enforce square using average + const size = (clampedWidth + clampedHeight) / 2; + + // Clamp position to keep square within bounds + const x = Math.max( + 0, + Math.min(newBox.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(newBox.y, imageSize.height - size), + ); + + return { + ...newBox, + x, + y, + width: size, + height: size, + }; + }} + /> + + +
    + ) : ( +
    + {t("wizard.step2.selectCameraPrompt")} +
    + )} +
    +
    +
    + +
    + + +
    +
    + ); +} diff --git a/web/src/components/classification/wizard/Step3ChooseExamples.tsx b/web/src/components/classification/wizard/Step3ChooseExamples.tsx new file mode 100644 index 000000000..e3dd04afc --- /dev/null +++ b/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -0,0 +1,628 @@ +import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; +import { useState, useEffect, useCallback, useMemo } from "react"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import axios from "axios"; +import { toast } from "sonner"; +import { Step1FormData } from "./Step1NameAndDefine"; +import { Step2FormData } from "./Step2StateArea"; +import useSWR from "swr"; +import { baseUrl } from "@/api/baseUrl"; +import { isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { IoIosWarning } from "react-icons/io"; + +export type Step3FormData = { + examplesGenerated: boolean; + imageClassifications?: { [imageName: string]: string }; +}; + +type Step3ChooseExamplesProps = { + step1Data: Step1FormData; + step2Data?: Step2FormData; + initialData?: Partial; + onClose: () => void; + onBack: () => void; +}; + +export default function Step3ChooseExamples({ + step1Data, + step2Data, + initialData, + onClose, + onBack, +}: Step3ChooseExamplesProps) { + const { t } = useTranslation(["views/classificationModel"]); + const [isGenerating, setIsGenerating] = useState(false); + const [hasGenerated, setHasGenerated] = useState( + initialData?.examplesGenerated || false, + ); + const [imageClassifications, setImageClassifications] = useState<{ + [imageName: string]: string; + }>(initialData?.imageClassifications || {}); + const [isTraining, setIsTraining] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [currentClassIndex, setCurrentClassIndex] = useState(0); + const [selectedImages, setSelectedImages] = useState>(new Set()); + const [cacheKey, setCacheKey] = useState(Date.now()); + const [loadedImages, setLoadedImages] = useState>(new Set()); + + const handleImageLoad = useCallback((imageName: string) => { + setLoadedImages((prev) => new Set(prev).add(imageName)); + }, []); + + const { data: trainImages, mutate: refreshTrainImages } = useSWR( + hasGenerated ? `classification/${step1Data.modelName}/train` : null, + ); + + const unknownImages = useMemo(() => { + if (!trainImages) return []; + return trainImages; + }, [trainImages]); + + const toggleImageSelection = useCallback((imageName: string) => { + setSelectedImages((prev) => { + const newSet = new Set(prev); + if (newSet.has(imageName)) { + newSet.delete(imageName); + } else { + newSet.add(imageName); + } + return newSet; + }); + }, []); + + // Get all classes (excluding "none" - it will be auto-assigned) + const allClasses = useMemo(() => { + return [...step1Data.classes]; + }, [step1Data.classes]); + + const currentClass = allClasses[currentClassIndex]; + + const processClassificationsAndTrain = useCallback( + async (classifications: { [imageName: string]: string }) => { + // Step 1: Create config for the new model + const modelConfig: { + enabled: boolean; + name: string; + threshold: number; + state_config?: { + cameras: Record; + motion: boolean; + }; + object_config?: { objects: string[]; classification_type: string }; + } = { + enabled: true, + name: step1Data.modelName, + threshold: 0.8, + }; + + if (step1Data.modelType === "state") { + // State model config + const cameras: Record = {}; + step2Data?.cameraAreas.forEach((area) => { + cameras[area.camera] = { + crop: area.crop, + }; + }); + + modelConfig.state_config = { + cameras, + motion: true, + }; + } else { + // Object model config + modelConfig.object_config = { + objects: step1Data.objectLabel ? [step1Data.objectLabel] : [], + classification_type: step1Data.objectType || "sub_label", + } as { objects: string[]; classification_type: string }; + } + + // Update config via config API + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${step1Data.modelName}`, + config_data: { + classification: { + custom: { + [step1Data.modelName]: modelConfig, + }, + }, + }, + }); + + // Step 2: Classify each image by moving it to the correct category folder + const categorizePromises = Object.entries(classifications).map( + ([imageName, className]) => { + if (!className) return Promise.resolve(); + return axios.post( + `/classification/${step1Data.modelName}/dataset/categorize`, + { + training_file: imageName, + category: className === "none" ? "none" : className, + }, + ); + }, + ); + await Promise.all(categorizePromises); + + // Step 2.5: Delete any unselected images from train folder + // For state models, all images must be classified, so unselected images should be removed + // For object models, unselected images are assigned to "none" so they're already categorized + if (step1Data.modelType === "state") { + try { + // Fetch current train images to see what's left after categorization + const trainImagesResponse = await axios.get( + `/classification/${step1Data.modelName}/train`, + ); + const remainingTrainImages = trainImagesResponse.data || []; + + const categorizedImageNames = new Set(Object.keys(classifications)); + const unselectedImages = remainingTrainImages.filter( + (imageName) => !categorizedImageNames.has(imageName), + ); + + if (unselectedImages.length > 0) { + await axios.post( + `/classification/${step1Data.modelName}/train/delete`, + { + ids: unselectedImages, + }, + ); + } + } catch (error) { + // Silently fail - unselected images will remain but won't cause issues + // since the frontend filters out images that don't match expected format + } + } + + // Step 2.6: Create empty folders for classes that don't have any images + // This ensures all classes are available in the dataset view later + const classesWithImages = new Set( + Object.values(classifications).filter((c) => c && c !== "none"), + ); + const emptyFolderPromises = step1Data.classes + .filter((className) => !classesWithImages.has(className)) + .map((className) => + axios.post( + `/classification/${step1Data.modelName}/dataset/${className}/create`, + ), + ); + await Promise.all(emptyFolderPromises); + + // Step 3: Determine if we should train + // For state models, we need ALL states to have examples (at least 2 states) + // For object models, we need at least 1 class with images (the rest go to "none") + const allStatesHaveExamplesForTraining = + step1Data.modelType !== "state" || + step1Data.classes.every((className) => + classesWithImages.has(className), + ); + const shouldTrain = + step1Data.modelType === "object" + ? classesWithImages.size >= 1 + : allStatesHaveExamplesForTraining && classesWithImages.size >= 2; + + // Step 4: Kick off training only if we have enough classes with images + if (shouldTrain) { + await axios.post(`/classification/${step1Data.modelName}/train`); + + toast.success(t("wizard.step3.trainingStarted"), { + closeButton: true, + }); + setIsTraining(true); + } else { + // Don't train - not all states have examples + toast.success(t("wizard.step3.modelCreated"), { + closeButton: true, + }); + setIsTraining(false); + onClose(); + } + }, + [step1Data, step2Data, t, onClose], + ); + + const handleContinueClassification = useCallback(async () => { + // Mark selected images with current class + const newClassifications = { ...imageClassifications }; + + // Handle user going back and de-selecting images + const imagesToCheck = unknownImages.slice(0, 24); + imagesToCheck.forEach((imageName) => { + if ( + newClassifications[imageName] === currentClass && + !selectedImages.has(imageName) + ) { + delete newClassifications[imageName]; + } + }); + + // Then, add all currently selected images to the current class + selectedImages.forEach((imageName) => { + newClassifications[imageName] = currentClass; + }); + + // Check if we're on the last class to select + const isLastClass = currentClassIndex === allClasses.length - 1; + + if (isLastClass) { + // For object models, assign remaining unclassified images to "none" + // For state models, this should never happen since we require all images to be classified + if (step1Data.modelType !== "state") { + unknownImages.slice(0, 24).forEach((imageName) => { + if (!newClassifications[imageName]) { + newClassifications[imageName] = "none"; + } + }); + } + + // All done, trigger training immediately + setImageClassifications(newClassifications); + setIsProcessing(true); + + try { + await processClassificationsAndTrain(newClassifications); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to classify images"; + + toast.error( + t("wizard.step3.errors.classifyFailed", { error: errorMessage }), + ); + setIsProcessing(false); + } + } else { + // Move to next class + setImageClassifications(newClassifications); + setCurrentClassIndex((prev) => prev + 1); + setSelectedImages(new Set()); + } + }, [ + selectedImages, + currentClass, + currentClassIndex, + allClasses, + imageClassifications, + unknownImages, + step1Data, + processClassificationsAndTrain, + t, + ]); + + const generateExamples = useCallback(async () => { + setIsGenerating(true); + + try { + if (step1Data.modelType === "state") { + // For state models, use cameras and crop areas + if (!step2Data?.cameraAreas || step2Data.cameraAreas.length === 0) { + toast.error(t("wizard.step3.errors.noCameras")); + setIsGenerating(false); + return; + } + + const cameras: { [key: string]: [number, number, number, number] } = {}; + step2Data.cameraAreas.forEach((area) => { + cameras[area.camera] = area.crop; + }); + + await axios.post("/classification/generate_examples/state", { + model_name: step1Data.modelName, + cameras, + }); + } else { + // For object models, use label + if (!step1Data.objectLabel) { + toast.error(t("wizard.step3.errors.noObjectLabel")); + setIsGenerating(false); + return; + } + + // For now, use all enabled cameras + // TODO: In the future, we might want to let users select specific cameras + await axios.post("/classification/generate_examples/object", { + model_name: step1Data.modelName, + label: step1Data.objectLabel, + }); + } + + setHasGenerated(true); + toast.success(t("wizard.step3.generateSuccess")); + + // Update cache key to force image reload + setCacheKey(Date.now()); + await refreshTrainImages(); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to generate examples"; + + toast.error( + t("wizard.step3.errors.generateFailed", { error: errorMessage }), + ); + } finally { + setIsGenerating(false); + } + }, [step1Data, step2Data, t, refreshTrainImages]); + + useEffect(() => { + if (!hasGenerated && !isGenerating) { + generateExamples(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleContinue = useCallback(async () => { + setIsProcessing(true); + try { + await processClassificationsAndTrain(imageClassifications); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to classify images"; + + toast.error( + t("wizard.step3.errors.classifyFailed", { error: errorMessage }), + ); + setIsProcessing(false); + } + }, [imageClassifications, processClassificationsAndTrain, t]); + + const unclassifiedImages = useMemo(() => { + if (!unknownImages) return []; + const images = unknownImages.slice(0, 24); + + // Only filter if we have any classifications + if (Object.keys(imageClassifications).length === 0) { + return images; + } + + // If we're viewing a previous class (going back), show images for that class + // Otherwise show only unclassified images + const currentClassInView = allClasses[currentClassIndex]; + return images.filter((img) => { + const imgClass = imageClassifications[img]; + // Show if: unclassified OR classified with current class we're viewing + return !imgClass || imgClass === currentClassInView; + }); + }, [unknownImages, imageClassifications, allClasses, currentClassIndex]); + + const allImagesClassified = useMemo(() => { + return unclassifiedImages.length === 0; + }, [unclassifiedImages]); + + const isLastClass = currentClassIndex === allClasses.length - 1; + const statesWithExamples = useMemo(() => { + if (step1Data.modelType !== "state") return new Set(); + + const states = new Set(); + const allImages = unknownImages.slice(0, 24); + + // Check which states have at least one image classified + allImages.forEach((img) => { + let className: string | undefined; + if (selectedImages.has(img)) { + className = currentClass; + } else { + className = imageClassifications[img]; + } + if (className && allClasses.includes(className)) { + states.add(className); + } + }); + + return states; + }, [ + step1Data.modelType, + unknownImages, + imageClassifications, + selectedImages, + currentClass, + allClasses, + ]); + + const allStatesHaveExamples = useMemo(() => { + if (step1Data.modelType !== "state") return true; + return allClasses.every((className) => statesWithExamples.has(className)); + }, [step1Data.modelType, allClasses, statesWithExamples]); + + const hasUnclassifiedImages = useMemo(() => { + if (!unknownImages) return false; + const allImages = unknownImages.slice(0, 24); + return allImages.some((img) => !imageClassifications[img]); + }, [unknownImages, imageClassifications]); + + const showMissingStatesWarning = useMemo(() => { + return ( + step1Data.modelType === "state" && + isLastClass && + !allStatesHaveExamples && + !hasUnclassifiedImages && + hasGenerated + ); + }, [ + step1Data.modelType, + isLastClass, + allStatesHaveExamples, + hasUnclassifiedImages, + hasGenerated, + ]); + + const handleBack = useCallback(() => { + if (currentClassIndex > 0) { + const previousClass = allClasses[currentClassIndex - 1]; + setCurrentClassIndex((prev) => prev - 1); + + // Restore selections for the previous class + const previousSelections = Object.entries(imageClassifications) + .filter(([_, className]) => className === previousClass) + .map(([imageName, _]) => imageName); + setSelectedImages(new Set(previousSelections)); + } else { + onBack(); + } + }, [currentClassIndex, allClasses, imageClassifications, onBack]); + + return ( +
    + {isTraining ? ( +
    + +
    +

    + {t("wizard.step3.training.title")} +

    +

    + {t("wizard.step3.training.description")} +

    +
    + +
    + ) : isGenerating ? ( +
    + +
    +

    + {t("wizard.step3.generating.title")} +

    +

    + {t("wizard.step3.generating.description")} +

    +
    +
    + ) : hasGenerated ? ( +
    + {showMissingStatesWarning && ( + + + + {t("wizard.step3.missingStatesWarning.title")} + + + {t("wizard.step3.missingStatesWarning.description")} + + + )} + {!allImagesClassified && ( +
    +

    + {t("wizard.step3.selectImagesPrompt", { + className: currentClass, + })} +

    +

    + {t("wizard.step3.selectImagesDescription")} +

    +
    + )} +
    + {!unknownImages || unknownImages.length === 0 ? ( +
    +

    + {t("wizard.step3.noImages")} +

    + +
    + ) : allImagesClassified && isProcessing ? ( +
    + +

    + {t("wizard.step3.classifying")} +

    +
    + ) : ( +
    + {unclassifiedImages.map((imageName, index) => { + const isSelected = selectedImages.has(imageName); + return ( +
    toggleImageSelection(imageName)} + > + {!loadedImages.has(imageName) && ( +
    + +
    + )} + {`Example handleImageLoad(imageName)} + /> +
    + ); + })} +
    + )} +
    +
    + ) : ( +
    +

    + {t("wizard.step3.errors.generationFailed")} +

    + +
    + )} + + {!isTraining && ( +
    + + +
    + )} +
    + ); +} diff --git a/web/src/components/dynamic/CameraFeatureToggle.tsx b/web/src/components/dynamic/CameraFeatureToggle.tsx index 122178edb..5479e4297 100644 --- a/web/src/components/dynamic/CameraFeatureToggle.tsx +++ b/web/src/components/dynamic/CameraFeatureToggle.tsx @@ -6,6 +6,7 @@ import { } from "@/components/ui/tooltip"; import { isDesktop } from "react-device-detect"; import { cn } from "@/lib/utils"; +import ActivityIndicator from "../indicators/activity-indicator"; const variants = { primary: { @@ -30,7 +31,8 @@ type CameraFeatureToggleProps = { Icon: IconType; title: string; onClick?: () => void; - disabled?: boolean; // New prop for disabling + disabled?: boolean; + loading?: boolean; }; export default function CameraFeatureToggle({ @@ -40,7 +42,8 @@ export default function CameraFeatureToggle({ Icon, title, onClick, - disabled = false, // Default to false + disabled = false, + loading = false, }: CameraFeatureToggleProps) { const content = (
    - + {loading ? ( + + ) : ( + + )}
    ); diff --git a/web/src/components/filter/CalendarFilterButton.tsx b/web/src/components/filter/CalendarFilterButton.tsx index 876eb9ab0..9f052b73d 100644 --- a/web/src/components/filter/CalendarFilterButton.tsx +++ b/web/src/components/filter/CalendarFilterButton.tsx @@ -18,6 +18,7 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; type CalendarFilterButtonProps = { reviewSummary?: ReviewSummary; @@ -105,6 +106,7 @@ export function CalendarRangeFilterButton({ const { t } = useTranslation(["components/filter"]); const { data: config } = useSWR("config"); const timezone = useTimezone(config); + const [weekStartsOn] = useUserPersistence("weekStartsOn", 0); const [open, setOpen] = useState(false); const selectedDate = useFormattedRange( @@ -138,6 +140,7 @@ export function CalendarRangeFilterButton({ initialDateTo={range?.to} timezone={timezone} showCompare={false} + weekStartsOn={weekStartsOn} onUpdate={(range) => { updateSelectedRange(range.range); setOpen(false); diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 6d9ea7856..14845fdb8 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -7,9 +7,8 @@ import { import { isDesktop, isMobile } from "react-device-detect"; import useSWR from "swr"; import { MdHome } from "react-icons/md"; -import { usePersistedOverlayState } from "@/hooks/use-overlay-state"; import { Button, buttonVariants } from "../ui/button"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { LuPencil, LuPlus } from "react-icons/lu"; import { @@ -57,7 +56,7 @@ import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import ActivityIndicator from "../indicators/activity-indicator"; import { ScrollArea, ScrollBar } from "../ui/scroll-area"; -import { usePersistence } from "@/hooks/use-persistence"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { cn } from "@/lib/utils"; import * as LuIcons from "react-icons/lu"; @@ -71,12 +70,15 @@ import { MobilePageTitle, } from "../mobile/MobilePage"; -import { Label } from "../ui/label"; import { Switch } from "../ui/switch"; import { CameraStreamingDialog } from "../settings/CameraStreamingDialog"; import { DialogTrigger } from "@radix-ui/react-dialog"; import { useStreamingSettings } from "@/context/streaming-settings-provider"; import { Trans, useTranslation } from "react-i18next"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state"; type CameraGroupSelectorProps = { className?: string; @@ -85,6 +87,8 @@ type CameraGroupSelectorProps = { export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { const { t } = useTranslation(["components/camera"]); const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); + const isAdmin = useIsAdmin(); // tooltip @@ -105,9 +109,9 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { [timeoutId], ); - // groups + // groups - use user-namespaced key for persistence to avoid cross-user conflicts - const [group, setGroup, deleteGroup] = usePersistedOverlayState( + const [group, setGroup, , deleteGroup] = useUserPersistedOverlayState( "cameraGroup", "default" as string, ); @@ -117,10 +121,22 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { return []; } - return Object.entries(config.camera_groups).sort( - (a, b) => a[1].order - b[1].order, - ); - }, [config]); + const allGroups = Object.entries(config.camera_groups); + + // If custom role, filter out groups where user has no accessible cameras + if (!isAdmin) { + return allGroups + .filter(([, groupConfig]) => { + // Check if user has access to at least one camera in this group + return groupConfig.cameras.some((cameraName) => + allowedCameras.includes(cameraName), + ); + }) + .sort((a, b) => a[1].order - b[1].order); + } + + return allGroups.sort((a, b) => a[1].order - b[1].order); + }, [config, allowedCameras, isAdmin]); // add group @@ -137,6 +153,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { activeGroup={group} setGroup={setGroup} deleteGroup={deleteGroup} + isAdmin={isAdmin} />
    setAddGroup(true)} - > - - + {isAdmin && ( + + )} {isMobile && }
    @@ -226,6 +245,7 @@ type NewGroupDialogProps = { activeGroup?: string; setGroup: (value: string | undefined, replace?: boolean | undefined) => void; deleteGroup: () => void; + isAdmin?: boolean; }; function NewGroupDialog({ open, @@ -234,6 +254,7 @@ function NewGroupDialog({ activeGroup, setGroup, deleteGroup, + isAdmin, }: NewGroupDialogProps) { const { t } = useTranslation(["components/camera"]); const { mutate: updateConfig } = useSWR("config"); @@ -255,10 +276,16 @@ function NewGroupDialog({ const [editState, setEditState] = useState<"none" | "add" | "edit">("none"); const [isLoading, setIsLoading] = useState(false); - const [, , , deleteGridLayout] = usePersistence( + const [, , , deleteGridLayout] = useUserPersistence( `${activeGroup}-draggable-layout`, ); + useEffect(() => { + if (!open) { + setEditState("none"); + } + }, [open]); + // callbacks const onDeleteGroup = useCallback( @@ -347,13 +374,7 @@ function NewGroupDialog({ position="top-center" closeButton={true} /> - { - setEditState("none"); - setOpen(open); - }} - > + {t("group.label")} {t("group.edit")} -
    - -
    + +
    + )}
    {currentGroups.map((group) => ( @@ -399,6 +422,7 @@ function NewGroupDialog({ group={group} onDeleteGroup={() => onDeleteGroup(group[0])} onEditGroup={() => onEditGroup(group)} + isReadOnly={!isAdmin} /> ))}
    @@ -510,12 +534,14 @@ type CameraGroupRowProps = { group: [string, CameraGroupConfig]; onDeleteGroup: () => void; onEditGroup: () => void; + isReadOnly?: boolean; }; export function CameraGroupRow({ group, onDeleteGroup, onEditGroup, + isReadOnly, }: CameraGroupRowProps) { const { t } = useTranslation(["components/camera"]); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -562,7 +588,7 @@ export function CameraGroupRow({ - {isMobile && ( + {isMobile && !isReadOnly && ( <> @@ -587,7 +613,7 @@ export function CameraGroupRow({ )} - {!isMobile && ( + {!isMobile && !isReadOnly && (
    @@ -650,6 +676,9 @@ export function CameraGroupEdit({ allGroupsStreamingSettings[editingGroup?.[0] ?? ""], ); + const allowedCameras = useAllowedCameras(); + const isAdmin = useIsAdmin(); + const [openCamera, setOpenCamera] = useState(); const birdseyeConfig = useMemo(() => config?.birdseye, [config]); @@ -837,21 +866,25 @@ export function CameraGroupEdit({ {t("group.cameras.desc")} {[ - ...(birdseyeConfig?.enabled ? ["birdseye"] : []), - ...Object.keys(config?.cameras ?? {}).sort( - (a, b) => - (config?.cameras[a]?.ui?.order ?? 0) - - (config?.cameras[b]?.ui?.order ?? 0), - ), + ...(birdseyeConfig?.enabled && + (isAdmin || "birdseye" in allowedCameras) + ? ["birdseye"] + : []), + ...Object.keys(config?.cameras ?? {}) + .filter((camera) => allowedCameras.includes(camera)) + .sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), ].map((camera) => (
    - + camera={camera} + />
    {camera !== "birdseye" && ( diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 247555a0a..cc89e13cf 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -13,6 +13,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import FilterSwitch from "./FilterSwitch"; import { FaVideo } from "react-icons/fa"; import { useTranslation } from "react-i18next"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; type CameraFilterButtonProps = { allCameras: string[]; @@ -35,6 +36,30 @@ export function CamerasFilterButton({ const [currentCameras, setCurrentCameras] = useState( selectedCameras, ); + const allowedCameras = useAllowedCameras(); + + // Filter cameras to only include those the user has access to + const filteredCameras = useMemo( + () => allCameras.filter((camera) => allowedCameras.includes(camera)), + [allCameras, allowedCameras], + ); + + // Filter groups to only include those with at least one allowed camera + const filteredGroups = useMemo( + () => + groups + .map(([name, config]) => { + const allowedGroupCameras = config.cameras.filter((camera) => + allowedCameras.includes(camera), + ); + return [name, { ...config, cameras: allowedGroupCameras }] as [ + string, + CameraGroupConfig, + ]; + }) + .filter(([, config]) => config.cameras.length > 0), + [groups, allowedCameras], + ); const buttonText = useMemo(() => { if (isMobile) { @@ -79,8 +104,8 @@ export function CamerasFilterButton({ ); const content = ( void; }; export default function FilterSwitch({ label, disabled = false, isChecked, + type = "", + extraValue = "", onCheckedChange, }: FilterSwitchProps) { return (
    - + {type === "camera" ? ( + + ) : type === "zone" ? ( + + ) : ( + + )} void; + selectedReviews: ReviewSegment[]; + setSelectedReviews: (reviews: ReviewSegment[]) => void; onExport: (id: string) => void; pullLatestData: () => void; }; @@ -32,19 +34,29 @@ export default function ReviewActionGroup({ pullLatestData, }: ReviewActionGroupProps) { const { t } = useTranslation(["components/dialog"]); + const isAdmin = useIsAdmin(); const onClearSelected = useCallback(() => { setSelectedReviews([]); }, [setSelectedReviews]); - const onMarkAsReviewed = useCallback(async () => { - await axios.post(`reviews/viewed`, { ids: selectedReviews }); + const allReviewed = selectedReviews.every( + (review) => review.has_been_reviewed, + ); + + const onToggleReviewed = useCallback(async () => { + const ids = selectedReviews.map((review) => review.id); + await axios.post(`reviews/viewed`, { + ids, + reviewed: !allReviewed, + }); setSelectedReviews([]); pullLatestData(); - }, [selectedReviews, setSelectedReviews, pullLatestData]); + }, [selectedReviews, setSelectedReviews, pullLatestData, allReviewed]); const onDelete = useCallback(() => { + const ids = selectedReviews.map((review) => review.id); axios - .post(`reviews/delete`, { ids: selectedReviews }) + .post(`reviews/delete`, { ids }) .then((resp) => { if (resp.status === 200) { toast.success(t("recording.confirmDelete.toast.success"), { @@ -75,6 +87,7 @@ export default function ReviewActionGroup({ useKeyboardListener(["Shift"], (_, modifiers) => { setBypassDialog(modifiers.shift); + return false; }); const handleDelete = useCallback(() => { @@ -139,7 +152,7 @@ export default function ReviewActionGroup({ aria-label={t("recording.button.export")} size="sm" onClick={() => { - onExport(selectedReviews[0]); + onExport(selectedReviews[0].id); onClearSelected(); }} > @@ -153,32 +166,44 @@ export default function ReviewActionGroup({ )} - + {isAdmin && ( + + )}
    diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index f2234b359..76274ec3f 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -25,6 +25,7 @@ import { CamerasFilterButton } from "./CamerasFilterButton"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import { useTranslation } from "react-i18next"; import { getTranslatedLabel } from "@/utils/i18n"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; const REVIEW_FILTERS = [ "cameras", @@ -72,6 +73,7 @@ export default function ReviewFilterGroup({ setMotionOnly, }: ReviewFilterGroupProps) { const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); const allLabels = useMemo(() => { if (filterList?.labels) { @@ -83,7 +85,9 @@ export default function ReviewFilterGroup({ } const labels = new Set(); - const cameras = filter?.cameras || Object.keys(config.cameras); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); cameras.forEach((camera) => { if (camera == "birdseye") { @@ -106,7 +110,7 @@ export default function ReviewFilterGroup({ }); return [...labels].sort(); - }, [config, filterList, filter]); + }, [config, filterList, filter, allowedCameras]); const allZones = useMemo(() => { if (filterList?.zones) { @@ -118,7 +122,9 @@ export default function ReviewFilterGroup({ } const zones = new Set(); - const cameras = filter?.cameras || Object.keys(config.cameras); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); cameras.forEach((camera) => { if (camera == "birdseye") { @@ -134,11 +140,11 @@ export default function ReviewFilterGroup({ }); return [...zones].sort(); - }, [config, filterList, filter]); + }, [config, filterList, filter, allowedCameras]); const filterValues = useMemo( () => ({ - cameras: Object.keys(config?.cameras ?? {}).sort( + cameras: allowedCameras.sort( (a, b) => (config?.cameras[a]?.ui?.order ?? 0) - (config?.cameras[b]?.ui?.order ?? 0), @@ -146,7 +152,7 @@ export default function ReviewFilterGroup({ labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), }), - [config, allLabels, allZones], + [config, allLabels, allZones, allowedCameras], ); const groups = useMemo(() => { @@ -448,6 +454,24 @@ export function GeneralFilterContent({ onClose, }: GeneralFilterContentProps) { const { t } = useTranslation(["components/filter", "views/events"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const allAudioListenLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return [...labels].sort(); + }, [config]); return ( <>
    @@ -489,8 +513,7 @@ export function GeneralFilterContent({ checked={filter.labels === undefined} onCheckedChange={(isChecked) => { if (isChecked) { - const { labels: _labels, ...rest } = filter; - onUpdateFilter(rest); + onUpdateFilter({ ...filter, labels: undefined }); } }} /> @@ -499,7 +522,10 @@ export function GeneralFilterContent({ {allLabels.map((item) => ( { if (isChecked) { @@ -536,8 +562,7 @@ export function GeneralFilterContent({ checked={filter.zones === undefined} onCheckedChange={(isChecked) => { if (isChecked) { - const { zones: _zones, ...rest } = filter; - onUpdateFilter(rest); + onUpdateFilter({ ...filter, zones: undefined }); } }} /> @@ -546,7 +571,8 @@ export function GeneralFilterContent({ {allZones.map((item) => ( { if (isChecked) { diff --git a/web/src/components/filter/SearchActionGroup.tsx b/web/src/components/filter/SearchActionGroup.tsx index ad6d6ccc8..62a3dc648 100644 --- a/web/src/components/filter/SearchActionGroup.tsx +++ b/web/src/components/filter/SearchActionGroup.tsx @@ -16,18 +16,24 @@ import { import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { toast } from "sonner"; import { Trans, useTranslation } from "react-i18next"; +import { useIsAdmin } from "@/hooks/use-is-admin"; type SearchActionGroupProps = { selectedObjects: string[]; setSelectedObjects: (ids: string[]) => void; pullLatestData: () => void; + onSelectAllObjects: () => void; + totalItems: number; }; export default function SearchActionGroup({ selectedObjects, setSelectedObjects, pullLatestData, + onSelectAllObjects, + totalItems, }: SearchActionGroupProps) { const { t } = useTranslation(["components/filter"]); + const isAdmin = useIsAdmin(); const onClearSelected = useCallback(() => { setSelectedObjects([]); }, [setSelectedObjects]); @@ -62,6 +68,7 @@ export default function SearchActionGroup({ useKeyboardListener(["Shift"], (_, modifiers) => { setBypassDialog(modifiers.shift); + return false; }); const handleDelete = useCallback(() => { @@ -121,24 +128,37 @@ export default function SearchActionGroup({ > {t("button.unselect", { ns: "common" })}
    -
    -
    - + + )}
    + {isAdmin && ( +
    + +
    + )}
    ); diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx index 1702fcc2a..fe9a70e18 100644 --- a/web/src/components/filter/SearchFilterGroup.tsx +++ b/web/src/components/filter/SearchFilterGroup.tsx @@ -24,9 +24,9 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog"; import { CalendarRangeFilterButton } from "./CalendarFilterButton"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; - import { useTranslation } from "react-i18next"; import { getTranslatedLabel } from "@/utils/i18n"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; type SearchFilterGroupProps = { className: string; @@ -46,6 +46,7 @@ export default function SearchFilterGroup({ const { data: config } = useSWR("config", { revalidateOnFocus: false, }); + const allowedCameras = useAllowedCameras(); const allLabels = useMemo(() => { if (filterList?.labels) { @@ -57,7 +58,9 @@ export default function SearchFilterGroup({ } const labels = new Set(); - const cameras = filter?.cameras || Object.keys(config.cameras); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); cameras.forEach((camera) => { if (camera == "birdseye") { @@ -87,7 +90,7 @@ export default function SearchFilterGroup({ }); return [...labels].sort(); - }, [config, filterList, filter]); + }, [config, filterList, filter, allowedCameras]); const allZones = useMemo(() => { if (filterList?.zones) { @@ -99,7 +102,9 @@ export default function SearchFilterGroup({ } const zones = new Set(); - const cameras = filter?.cameras || Object.keys(config.cameras); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); cameras.forEach((camera) => { if (camera == "birdseye") { @@ -118,16 +123,16 @@ export default function SearchFilterGroup({ }); return [...zones].sort(); - }, [config, filterList, filter]); + }, [config, filterList, filter, allowedCameras]); const filterValues = useMemo( () => ({ - cameras: Object.keys(config?.cameras || {}), + cameras: allowedCameras, labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), search_type: ["thumbnail", "description"] as SearchSource[], }), - [config, allLabels, allZones], + [allLabels, allZones, allowedCameras], ); const availableSortTypes = useMemo(() => { @@ -246,11 +251,30 @@ function GeneralFilterButton({ updateLabelFilter, }: GeneralFilterButtonProps) { const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); const [open, setOpen] = useState(false); const [currentLabels, setCurrentLabels] = useState( selectedLabels, ); + const allAudioListenLabels = useMemo>(() => { + if (!config) { + return new Set(); + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return labels; + }, [config]); + const buttonText = useMemo(() => { if (isMobile) { return t("labels.all.short"); @@ -261,13 +285,17 @@ function GeneralFilterButton({ } if (selectedLabels.length == 1) { - return getTranslatedLabel(selectedLabels[0]); + const label = selectedLabels[0]; + return getTranslatedLabel( + label, + allAudioListenLabels.has(label) ? "audio" : "object", + ); } return t("labels.count", { count: selectedLabels.length, }); - }, [selectedLabels, t]); + }, [selectedLabels, allAudioListenLabels, t]); // ui @@ -309,11 +337,10 @@ function GeneralFilterButton({ { if (!open) { @@ -343,6 +370,26 @@ export function GeneralFilterContent({ onClose, }: GeneralFilterContentProps) { const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const allAudioListenLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return [...labels].sort(); + }, [config]); + return ( <>
    @@ -368,7 +415,10 @@ export function GeneralFilterContent({ {allLabels.map((item) => ( { if (isChecked) { @@ -482,11 +532,10 @@ function SortTypeButton({ { if (!open) { @@ -544,9 +593,8 @@ export function SortTypeContent({ className="w-full space-y-1" > {availableSortTypes.map((value) => ( -
    +
    + {steps.map((_, idx) => ( +
    idx + ? "bg-muted-foreground" + : "bg-muted", + )} + /> + ))} +
    + ); + } + + // Default variant (original behavior) return ( -
    +
    {steps.map((name, idx) => (
    (null); + const dropzoneRef = useRef(null); + + // Auto focus the dropzone + useEffect(() => { + if (dropzoneRef.current && !preview) { + dropzoneRef.current.focus(); + } + }, [preview]); + + // Clean up preview URL on unmount or preview change + useEffect(() => { + return () => { + if (preview) { + URL.revokeObjectURL(preview); + } + }; + }, [preview]); const formSchema = z.object({ file: z @@ -52,9 +69,6 @@ export default function ImageEntry({ // Create preview const objectUrl = URL.createObjectURL(file); setPreview(objectUrl); - - // Clean up preview URL when component unmounts - return () => URL.revokeObjectURL(objectUrl); } }, [form], @@ -68,6 +82,31 @@ export default function ImageEntry({ multiple: false, }); + const handlePaste = useCallback( + (event: React.ClipboardEvent) => { + event.preventDefault(); + const clipboardItems = Array.from(event.clipboardData.items); + for (const item of clipboardItems) { + if (item.type.startsWith("image/")) { + const blob = item.getAsFile(); + if (blob && blob.size <= maxSize) { + const mimeType = blob.type.split("/")[1]; + const extension = `.${mimeType}`; + if (accept["image/*"].includes(extension)) { + const fileName = blob.name || `pasted-image.${mimeType}`; + const file = new File([blob], fileName, { type: blob.type }); + form.setValue("file", file, { shouldValidate: true }); + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + return; // Take the first valid image + } + } + } + } + }, + [form, maxSize, accept], + ); + const onSubmit = useCallback( (data: z.infer) => { if (!data.file) return; @@ -90,7 +129,12 @@ export default function ImageEntry({ render={() => ( -
    +
    {!preview ? (
    >(() => { + if (!config) { + return new Set(); + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return labels; + }, [config]); + + const translatedAudioLabelMap = useMemo>(() => { + const map = new Map(); + if (!config) return map; + + allAudioListenLabels.forEach((label) => { + // getTranslatedLabel likely depends on i18n internally; including `lang` + // in deps ensures this map is rebuilt when language changes + map.set(label, getTranslatedLabel(label, "audio")); + }); + return map; + }, [allAudioListenLabels, config]); + + function resolveLabel(value: string) { + const mapped = translatedAudioLabelMap.get(value); + if (mapped) return mapped; + return getTranslatedLabel( + value, + allAudioListenLabels.has(value) ? "audio" : "object", + ); + } + const [inputValue, setInputValue] = useState(search || ""); const [currentFilterType, setCurrentFilterType] = useState( null, @@ -90,9 +128,8 @@ export default function InputWithTags({ // TODO: search history from browser storage - const [searchHistory, setSearchHistory, searchHistoryLoaded] = usePersistence< - SavedSearchQuery[] - >("frigate-search-history"); + const [searchHistory, setSearchHistory, searchHistoryLoaded] = + useUserPersistence("frigate-search-history"); const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -362,7 +399,7 @@ export default function InputWithTags({ newFilters.sort = value as SearchSortType; break; default: - // Handle array types (cameras, labels, subLabels, zones) + // Handle array types (cameras, labels, sub_labels, attributes, zones) if (!newFilters[type]) newFilters[type] = []; if (Array.isArray(newFilters[type])) { if (!(newFilters[type] as string[]).includes(value)) { @@ -420,7 +457,8 @@ export default function InputWithTags({ ? t("button.yes", { ns: "common" }) : t("button.no", { ns: "common" }); } else if (filterType === "labels") { - return getTranslatedLabel(String(filterValues)); + const value = String(filterValues); + return resolveLabel(value); } else if (filterType === "search_type") { return t("filter.searchType." + String(filterValues)); } else { @@ -826,9 +864,15 @@ export default function InputWithTags({ className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm text-green-800 smart-capitalize" > {t("filter.label." + filterType)}:{" "} - {filterType === "labels" - ? getTranslatedLabel(value) - : value.replaceAll("_", " ")} + {filterType === "labels" ? ( + resolveLabel(value) + ) : filterType === "cameras" ? ( + + ) : filterType === "zones" ? ( + + ) : ( + value.replaceAll("_", " ") + )}
    {children}
    + {actions && ( +
    + {actions} +
    + )}
    ); } @@ -215,7 +218,7 @@ export function MobilePageHeader({ type MobilePageTitleProps = React.HTMLAttributes; export function MobilePageTitle({ className, ...props }: MobilePageTitleProps) { - return

    ; + return

    ; } type MobilePageDescriptionProps = React.HTMLAttributes; diff --git a/web/src/components/navigation/NavItem.tsx b/web/src/components/navigation/NavItem.tsx index f8ee7eb0d..204e7a9dd 100644 --- a/web/src/components/navigation/NavItem.tsx +++ b/web/src/components/navigation/NavItem.tsx @@ -46,13 +46,13 @@ export default function NavItem({ onClick={onClick} className={({ isActive }) => cn( - "flex flex-col items-center justify-center rounded-lg", + "flex flex-col items-center justify-center rounded-lg p-[6px]", className, variants[item.variant ?? "primary"][isActive ? "active" : "inactive"], ) } > - + ); diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx index ce03bd8e2..a15d9c590 100644 --- a/web/src/components/overlay/CameraInfoDialog.tsx +++ b/web/src/components/overlay/CameraInfoDialog.tsx @@ -16,6 +16,7 @@ import axios from "axios"; import { toast } from "sonner"; import { Toaster } from "../ui/sonner"; import { Trans, useTranslation } from "react-i18next"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; type CameraInfoDialogProps = { camera: CameraConfig; @@ -74,6 +75,8 @@ export default function CameraInfoDialog({ return b === 0 ? a : gcd(b, a % b); } + const cameraName = useCameraFriendlyName(camera); + return ( <> @@ -85,7 +88,7 @@ export default function CameraInfoDialog({ {t("cameras.info.cameraProbeInfo", { - camera: camera.name.replaceAll("_", " "), + camera: cameraName, })} diff --git a/web/src/components/overlay/ClassificationSelectionDialog.tsx b/web/src/components/overlay/ClassificationSelectionDialog.tsx new file mode 100644 index 000000000..6398348a4 --- /dev/null +++ b/web/src/components/overlay/ClassificationSelectionDialog.tsx @@ -0,0 +1,153 @@ +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop, isMobile } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import React, { ReactNode, useCallback, useMemo, useState } from "react"; +import TextEntryDialog from "./dialog/TextEntryDialog"; +import { Button } from "../ui/button"; +import axios from "axios"; +import { toast } from "sonner"; +import { Separator } from "../ui/separator"; + +type ClassificationSelectionDialogProps = { + className?: string; + classes: string[]; + modelName: string; + image: string; + onRefresh: () => void; + children: ReactNode; +}; +export default function ClassificationSelectionDialog({ + className, + classes, + modelName, + image, + onRefresh, + children, +}: ClassificationSelectionDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + + const onCategorizeImage = useCallback( + (category: string) => { + axios + .post(`/classification/${modelName}/dataset/categorize`, { + category, + training_file: image, + }) + .then((resp) => { + if (resp.status == 200) { + toast.success(t("toast.success.categorizedImage"), { + position: "top-center", + }); + onRefresh(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.categorizeFailed", { errorMessage }), { + position: "top-center", + }); + }); + }, + [modelName, image, onRefresh, t], + ); + + const isChildButton = useMemo( + () => React.isValidElement(children) && children.type === Button, + [children], + ); + + // control + const [newClass, setNewClass] = useState(false); + + // components + const Selector = isDesktop ? DropdownMenu : Drawer; + const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; + const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent; + const SelectorItem = isDesktop + ? DropdownMenuItem + : (props: React.HTMLAttributes) => ( + +
    + + ); + + return ( +
    + onCategorizeImage(newCat)} + /> + + + + + {children} + + + {isMobile && ( + + Details + Details + + )} + {t("categorizeImageAs")} +
    + {classes.sort().map((category) => ( + onCategorizeImage(category)} + > + {category === "none" + ? t("details.none") + : category.replaceAll("_", " ")} + + ))} + + setNewClass(true)} + > + {t("createCategory.new")} + +
    +
    +
    + {t("categorizeImage")} +
    +
    + ); +} diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx new file mode 100644 index 000000000..0b10f1c9d --- /dev/null +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -0,0 +1,249 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useTranslation } from "react-i18next"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { isDesktop, isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "../mobile/MobilePage"; + +type CreateRoleOverlayProps = { + show: boolean; + config: FrigateConfig; + onCreate: (role: string, cameras: string[]) => void; + onCancel: () => void; +}; + +export default function CreateRoleDialog({ + show, + config, + onCreate, + onCancel, +}: CreateRoleOverlayProps) { + const { t } = useTranslation(["views/settings"]); + const [isLoading, setIsLoading] = useState(false); + + const cameras = Object.keys(config.cameras || {}); + + const existingRoles = Object.keys(config.auth?.roles || {}); + + const formSchema = z.object({ + role: z + .string() + .min(1, t("roles.dialog.form.role.roleIsRequired")) + .regex(/^[A-Za-z0-9._]+$/, { + message: t("roles.dialog.form.role.roleOnlyInclude"), + }) + .refine((role) => !existingRoles.includes(role), { + message: t("roles.dialog.form.role.roleExists"), + }), + cameras: z + .array(z.string()) + .min(1, t("roles.dialog.form.cameras.required")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + role: "", + cameras: [], + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await onCreate(values.role, values.cameras); + form.reset(); + } catch (error) { + // Error handled in parent + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!show) { + form.reset({ + role: "", + cameras: [], + }); + } + }, [show, form]); + + const handleCancel = () => { + form.reset({ + role: "", + cameras: [], + }); + onCancel(); + }; + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + return ( + + +
    + {t("roles.dialog.createRole.title")} + + {t("roles.dialog.createRole.desc")} + +
    + +
    + + ( + + + {t("roles.dialog.form.role.title")} + + + + + + {t("roles.dialog.form.role.desc")} + + + + )} + /> + +
    + {t("roles.dialog.form.cameras.title")} + + {t("roles.dialog.form.cameras.desc")} + +
    + {cameras.map((camera) => ( + { + return ( + +
    + + + +
    + + { + return checked + ? field.onChange([ + ...(field.value as string[]), + camera, + ]) + : field.onChange( + (field.value as string[])?.filter( + (value: string) => value !== camera, + ) || [], + ); + }} + /> + +
    + ); + }} + /> + ))} +
    + +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + ); +} diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx new file mode 100644 index 000000000..ef30c649d --- /dev/null +++ b/web/src/components/overlay/CreateTriggerDialog.tsx @@ -0,0 +1,472 @@ +import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import useSWR from "swr"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { FrigateConfig } from "@/types/frigateConfig"; +import ImagePicker from "@/components/overlay/ImagePicker"; +import { Trigger, TriggerAction, TriggerType } from "@/types/trigger"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "../ui/textarea"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { isDesktop, isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "../mobile/MobilePage"; +import NameAndIdFields from "@/components/input/NameAndIdFields"; + +type CreateTriggerDialogProps = { + show: boolean; + trigger: Trigger | null; + selectedCamera: string; + isLoading: boolean; + onCreate: ( + enabled: boolean, + name: string, + type: TriggerType, + data: string, + threshold: number, + actions: TriggerAction[], + friendly_name: string, + ) => void; + onEdit: (trigger: Trigger) => void; + onCancel: () => void; +}; + +export default function CreateTriggerDialog({ + show, + trigger, + selectedCamera, + isLoading, + onCreate, + onEdit, + onCancel, +}: CreateTriggerDialogProps) { + const { t } = useTranslation("views/settings"); + const { data: config } = useSWR("config"); + + const availableActions = useMemo(() => { + if (!config) return []; + + if (config.cameras[selectedCamera].notifications.enabled_in_config) { + return ["notification", "sub_label", "attribute"]; + } + return ["sub_label", "attribute"]; + }, [config, selectedCamera]); + + const existingTriggerNames = useMemo(() => { + if ( + !config || + !selectedCamera || + !config.cameras[selectedCamera]?.semantic_search?.triggers + ) { + return []; + } + return Object.keys(config.cameras[selectedCamera].semantic_search.triggers); + }, [config, selectedCamera]); + + const existingTriggerFriendlyNames = useMemo(() => { + if ( + !config || + !selectedCamera || + !config.cameras[selectedCamera]?.semantic_search?.triggers + ) { + return []; + } + return Object.values( + config.cameras[selectedCamera].semantic_search.triggers, + ).map((trigger) => trigger.friendly_name); + }, [config, selectedCamera]); + + const formSchema = z.object({ + enabled: z.boolean(), + name: z + .string() + .min(2, t("triggers.dialog.form.name.error.minLength")) + .regex( + /^[a-zA-Z0-9_-]+$/, + t("triggers.dialog.form.name.error.invalidCharacters"), + ) + .refine( + (value) => + !existingTriggerNames.includes(value) || value === trigger?.name, + t("triggers.dialog.form.name.error.alreadyExists"), + ), + friendly_name: z + .string() + .min(2, t("triggers.dialog.form.name.error.minLength")) + .refine( + (value) => + !existingTriggerFriendlyNames.includes(value) || + value === trigger?.friendly_name, + t("triggers.dialog.form.name.error.alreadyExists"), + ), + type: z.enum(["thumbnail", "description"]), + data: z.string().min(1, t("triggers.dialog.form.content.error.required")), + threshold: z + .number() + .min(0, t("triggers.dialog.form.threshold.error.min")) + .max(1, t("triggers.dialog.form.threshold.error.max")), + actions: z.array(z.enum(["notification", "sub_label", "attribute"])), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + enabled: trigger?.enabled ?? true, + name: trigger?.name ?? "", + friendly_name: trigger?.friendly_name ?? "", + type: trigger?.type ?? "description", + data: trigger?.data ?? "", + threshold: trigger?.threshold ?? 0.5, + actions: trigger?.actions ?? [], + }, + }); + + const onSubmit = async (values: z.infer) => { + if (trigger && existingTriggerNames.includes(trigger.name)) { + onEdit({ ...values }); + } else { + onCreate( + values.enabled, + values.name, + values.type, + values.data, + values.threshold, + values.actions, + values.friendly_name, + ); + } + }; + + useEffect(() => { + if (!show) { + form.reset({ + enabled: true, + name: "", + friendly_name: "", + type: "description", + data: "", + threshold: 0.5, + actions: [], + }); + } else if (trigger) { + form.reset( + { + enabled: trigger.enabled, + name: trigger.name, + friendly_name: trigger.friendly_name ?? trigger.name, + type: trigger.type, + data: trigger.data, + threshold: trigger.threshold, + actions: trigger.actions, + }, + { keepDirty: false, keepTouched: false }, // Reset validation state + ); + // Trigger validation to ensure isValid updates + // form.trigger(); + } + }, [show, trigger, form]); + + const handleCancel = () => { + form.reset(); + onCancel(); + }; + + const cameraName = useCameraFriendlyName(selectedCamera); + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + return ( + + +
    + + {t( + trigger + ? "triggers.dialog.editTrigger.title" + : "triggers.dialog.createTrigger.title", + )} + + + {t( + trigger + ? "triggers.dialog.editTrigger.desc" + : "triggers.dialog.createTrigger.desc", + { + camera: cameraName, + }, + )} + +
    + +
    + + + + ( + +
    + + {t("button.enabled", { ns: "common" })} + +
    + {t("triggers.dialog.form.enabled.description")} +
    +
    + + + +
    + )} + /> + + ( + + {t("triggers.dialog.form.type.title")} + + + + )} + /> + + ( + + + {t("triggers.dialog.form.content.title")} + + {form.watch("type") === "thumbnail" ? ( + <> + + + + + ) : ( + <> + +