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/.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..0c1fc1f53 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2020 Blake Blackshear +Copyright (c) 2025 Frigate LLC (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 2baac5aad..d1427b6df 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ 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 . \ diff --git a/README.md b/README.md index 35e8cb7e9..b1eab6c53 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 or AI accelerator such as a [Google Coral](https://coral.ai/products/) or [Hailo](https://hailo.ai/) is highly recommended. AI accelerators 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 LLC** 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 @@ -66,3 +77,7 @@ We use [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) to support la Translation status + +--- + +**Copyright © 2025 Frigate LLC.** diff --git a/TRADEMARK.md b/TRADEMARK.md new file mode 100644 index 000000000..ef3681cfa --- /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 LLC**: + +- **Frigate™** +- **Frigate NVR™** +- **Frigate+™** +- **The Frigate Logo** + +**Note on Common Law Rights:** +Frigate LLC 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 LLC. + +## 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 LLC. +- **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/docker/main/build_pysqlite3.sh b/docker/main/build_pysqlite3.sh index c84c6fcf7..14d0cde44 100755 --- a/docker/main/build_pysqlite3.sh +++ b/docker/main/build_pysqlite3.sh @@ -5,21 +5,27 @@ set -euxo pipefail SQLITE3_VERSION="3.46.1" PYSQLITE3_VERSION="0.5.3" +# Install libsqlite3-dev if not present (needed for some base images like NVIDIA TensorRT) +if ! dpkg -l | grep -q libsqlite3-dev; then + echo "Installing libsqlite3-dev for compilation..." + apt-get update && apt-get install -y libsqlite3-dev && rm -rf /var/lib/apt/lists/* +fi + # Fetch the pre-built sqlite amalgamation instead of building from source if [[ ! -d "sqlite" ]]; then mkdir sqlite cd sqlite - + # Download the pre-built amalgamation from sqlite.org # For SQLite 3.46.1, the amalgamation version is 3460100 SQLITE_AMALGAMATION_VERSION="3460100" - + wget https://www.sqlite.org/2024/sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}.zip -O sqlite-amalgamation.zip unzip sqlite-amalgamation.zip mv sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}/* . rmdir sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION} rm sqlite-amalgamation.zip - + cd ../ fi diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index c4fe56f03..330caff9f 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -20,6 +20,7 @@ apt-get -qq install --no-install-recommends -y \ libgl1 \ libglib2.0-0 \ libusb-1.0.0 \ + python3-h2 \ libgomp1 # memryx detector update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 @@ -95,6 +96,9 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then 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 @@ -115,6 +119,11 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then 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 @@ -136,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 index f96181ae0..676e06daa 100644 --- a/docker/main/install_memryx.sh +++ b/docker/main/install_memryx.sh @@ -2,9 +2,9 @@ set -e # Download the MxAccl for Frigate github release -wget https://github.com/memryx/mx_accl_frigate/archive/refs/heads/main.zip -O /tmp/mxaccl.zip +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-main /opt/mx_accl_frigate +mv /tmp/mx_accl_frigate-2.1.0 /opt/mx_accl_frigate rm /tmp/mxaccl.zip # Install Python dependencies diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 7c0dc1843..b28de5e6b 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -56,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.* diff --git a/docker/main/requirements.txt b/docker/main/requirements.txt index 3ae420d07..f1ba7d9ad 100644 --- a/docker/main/requirements.txt +++ b/docker/main/requirements.txt @@ -1,2 +1 @@ scikit-build == 0.18.* -nvidia-pyindex diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 32099771d..46241c5ab 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -73,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; @@ -105,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; @@ -274,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; @@ -302,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/memryx/user_installation.sh b/docker/memryx/user_installation.sh index 20c9b8ece..b92b7e3b1 100644 --- a/docker/memryx/user_installation.sh +++ b/docker/memryx/user_installation.sh @@ -24,10 +24,13 @@ 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 memx-drivers -echo "Installing memx-drivers..." +# Update and install specific SDK 2.1 packages +echo "Installing MemryX SDK 2.1 packages..." sudo apt update -sudo apt install -y memx-drivers +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 @@ -37,11 +40,5 @@ fi echo -e "\n\n\033[1;31mYOU MUST RESTART YOUR COMPUTER NOW\033[0m\n\n" -# Install other runtime packages -packages=("memx-accl" "mxa-manager") -for pkg in "${packages[@]}"; do - echo "Installing $pkg..." - sudo apt install -y "$pkg" -done +echo "MemryX SDK 2.1 installation complete!" -echo "MemryX installation complete!" diff --git a/docker/tensorrt/Dockerfile.amd64 b/docker/tensorrt/Dockerfile.amd64 index ef0295a96..cdf5df9ff 100644 --- a/docker/tensorrt/Dockerfile.amd64 +++ b/docker/tensorrt/Dockerfile.amd64 @@ -21,7 +21,7 @@ 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 tensorflow-cpu \ + pip3 uninstall -y onnxruntime \ && pip3 install -U /deps/trt-wheels/*.whl COPY --from=rootfs / / diff --git a/docker/tensorrt/Dockerfile.arm64 b/docker/tensorrt/Dockerfile.arm64 index 0ae9c38e9..dd3c5de5e 100644 --- a/docker/tensorrt/Dockerfile.arm64 +++ b/docker/tensorrt/Dockerfile.arm64 @@ -112,7 +112,7 @@ RUN apt-get update \ && apt-get install -y protobuf-compiler libprotobuf-dev \ && rm -rf /var/lib/apt/lists/* RUN --mount=type=bind,source=docker/tensorrt/requirements-models-arm64.txt,target=/requirements-tensorrt-models.txt \ - pip3 wheel --wheel-dir=/trt-model-wheels -r /requirements-tensorrt-models.txt + pip3 wheel --wheel-dir=/trt-model-wheels --no-deps -r /requirements-tensorrt-models.txt FROM wget AS jetson-ffmpeg ARG DEBIAN_FRONTEND @@ -145,7 +145,8 @@ COPY --from=trt-wheels /etc/TENSORRT_VER /etc/TENSORRT_VER RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ --mount=type=bind,from=trt-model-wheels,source=/trt-model-wheels,target=/deps/trt-model-wheels \ pip3 uninstall -y onnxruntime \ - && pip3 install -U /deps/trt-wheels/*.whl /deps/trt-model-wheels/*.whl \ + && pip3 install -U /deps/trt-wheels/*.whl \ + && pip3 install -U /deps/trt-model-wheels/*.whl \ && ldconfig WORKDIR /opt/frigate/ diff --git a/docker/tensorrt/requirements-amd64.txt b/docker/tensorrt/requirements-amd64.txt index a7853aeec..63c68b583 100644 --- a/docker/tensorrt/requirements-amd64.txt +++ b/docker/tensorrt/requirements-amd64.txt @@ -13,7 +13,6 @@ nvidia_cusolver_cu12==11.6.3.*; platform_machine == 'x86_64' 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' -tensorflow==2.19.*; platform_machine == 'x86_64' onnx==1.16.*; 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/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 02482e792..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. @@ -247,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/camera_specific.md b/docs/docs/configuration/camera_specific.md index b7a817ff8..802b265c1 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -164,13 +164,35 @@ According to [this discussion](https://github.com/blakeblackshear/frigate/issues Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels. The setup of main stream can be also done via RTSP, but isn't always reliable on all hardware versions. The example configuration is working with the oldest HW version RLN16-410 device with multiple types of cameras. +
+ Example Config + +:::tip + +Reolink's latest cameras support two way audio via go2rtc and other applications. It is important that the http-flv stream is still used for stability, a secondary rtsp stream can be added that will be using for the two way audio only. + +NOTE: The RTSP stream can not be prefixed with `ffmpeg:`, as go2rtc needs to handle the stream to support two way audio. + +Ensure HTTP is enabled in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). + +::: + ```yaml go2rtc: streams: + # example for connecting to a standard Reolink camera your_reolink_camera: - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" your_reolink_camera_sub: - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + # example for connectin to a Reolink camera that supports two way talk + your_reolink_camera_twt: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" + - "rtsp://username:password@reolink_ip/Preview_01_sub + your_reolink_camera_twt_sub: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + - "rtsp://username:password@reolink_ip/Preview_01_sub + # example for connecting to a Reolink NVR your_reolink_camera_via_nvr: - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_main.bcs&user=username&password=password" # channel numbers are 0-15 - "ffmpeg:your_reolink_camera_via_nvr#audio=aac" @@ -201,22 +223,7 @@ cameras: roles: - detect ``` - -#### Reolink Doorbell - -The reolink doorbell supports two way audio via go2rtc and other applications. It is important that the http-flv stream is still used for stability, a secondary rtsp stream can be added that will be using for the two way audio only. - -Ensure HTTP is enabled in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). - -```yaml -go2rtc: - streams: - your_reolink_doorbell: - - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" - - rtsp://reolink_ip/Preview_01_sub - your_reolink_doorbell_sub: - - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" -``` +
### Unifi Protect Cameras diff --git a/docs/docs/configuration/custom_classification/object_classification.md b/docs/docs/configuration/custom_classification/object_classification.md index a75aae31a..3d59b74f9 100644 --- a/docs/docs/configuration/custom_classification/object_classification.md +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -10,9 +10,19 @@ Object classification allows you to train a custom MobileNetV2 classification mo 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. -When running the `-tensorrt` image, Nvidia GPUs will automatically be used to accelerate training. -### Sub label vs Attribute +## 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**: @@ -25,6 +35,15 @@ When running the `-tensorrt` image, Nvidia GPUs will automatically be used to ac - Ideal when multiple attributes can coexist independently. - Example: Detecting if a `person` in a construction yard is wearing a helmet or not. +## 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 @@ -56,18 +75,22 @@ classification: ## Training the model -Creating and training the model is done within the Frigate UI using the `Classification` page. +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of two steps: -### Getting Started +### 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. Include a `none` class for objects that don't fit any specific category. + +### 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. -// TODO add this section once UI is implemented. Explain process of selecting objects and curating training examples. - ### Improving the Model - **Problem framing**: Keep classes visually distinct and relevant to the chosen object types. -- **Data collection**: Use the model’s Train tab to gather balanced examples across times of day, weather, and distances. +- **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. diff --git a/docs/docs/configuration/custom_classification/state_classification.md b/docs/docs/configuration/custom_classification/state_classification.md index ec38ea696..66d3e60ca 100644 --- a/docs/docs/configuration/custom_classification/state_classification.md +++ b/docs/docs/configuration/custom_classification/state_classification.md @@ -10,7 +10,17 @@ State classification allows you to train a custom MobileNetV2 classification mod 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. -When running the `-tensorrt` image, Nvidia GPUs will automatically be used to accelerate training. + +## 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 @@ -38,15 +48,25 @@ classification: ## Training the model -Creating and training the model is done within the Frigate UI using the `Classification` page. +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps: -### Getting Started +### Step 1: Name and Define -When choosing a portion of the camera frame for state classification, it is important to make the crop tight around the area of interest to avoid extra signals unrelated to what is being classified. +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. -// TODO add this section once UI is implemented. Explain process of selecting a crop. +### 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. + +**Important**: All images must be assigned to a state before training can begin. This includes images that may not be optimal, such as when people temporarily block the view, sun glare is present, or other distractions occur. Assign these images to the state that is actually present (based on what you know the state to be), not based on the distraction. This training helps the model correctly identify the state even when such conditions occur during inference. + +Once all 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 Train tab to gather balanced examples across times of day and weather. +- **Data collection**: Use the model’s Recent Classifications tab to gather balanced examples across times of day and weather. diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md index d14946eaf..713671a16 100644 --- a/docs/docs/configuration/face_recognition.md +++ b/docs/docs/configuration/face_recognition.md @@ -70,7 +70,7 @@ 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). @@ -114,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. @@ -140,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: @@ -161,6 +161,8 @@ Start with the [Usage](#usage) section and re-read the [Model Requirements](#mod Accuracy is definitely a going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work. +Some users have also noted that setting the stream in camera firmware to a constant bit rate (CBR) leads to better image clarity than with a variable bit rate (VBR). + ### Why can't I bulk upload photos? It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to over-fitting in that particular scenario and hurt recognition performance. @@ -186,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 957922dbd..018dc2050 100644 --- a/docs/docs/configuration/genai.md +++ b/docs/docs/configuration/genai.md @@ -17,18 +17,17 @@ To use Generative AI, you must define a single provider at the global level of y genai: provider: gemini api_key: "{FRIGATE_GEMINI_API_KEY}" - model: gemini-1.5-flash + model: gemini-2.0-flash cameras: front_camera: - objects: genai: - enabled: True # <- enable GenAI for your front camera - use_snapshot: True - objects: - - person - required_zones: - - steps + enabled: True # <- enable GenAI for your front camera + use_snapshot: True + objects: + - person + required_zones: + - steps indoor_camera: objects: genai: @@ -71,7 +70,7 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru genai: provider: ollama base_url: http://localhost:11434 - model: llava:7b + model: qwen3-vl:4b ``` ## Google Gemini @@ -80,7 +79,7 @@ Google Gemini has a free tier allowing [15 queries per minute](https://ai.google ### 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`. +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini). ### Get API Key @@ -97,7 +96,7 @@ To start using Gemini, you must first get an API key from [Google AI Studio](htt genai: provider: gemini api_key: "{FRIGATE_GEMINI_API_KEY}" - model: gemini-1.5-flash + model: gemini-2.0-flash ``` :::note @@ -112,7 +111,7 @@ OpenAI does not have a free tier for their API. With the release of gpt-4o, pric ### 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`. +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models). ### Get API Key @@ -139,18 +138,19 @@ Microsoft offers several vision models through Azure OpenAI. A subscription is r ### 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`. +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). ### Create Resource and Get API Key -To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key 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. +To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key, model name, and resource URL, which must include the `api-version` parameter (see the example below). ### Configuration ```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 + base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview + model: gpt-5-mini api_key: "{FRIGATE_OPENAI_API_KEY}" ``` @@ -196,10 +196,10 @@ genai: model: llava 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." + 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. diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md index bf72db4ea..7e5618b5b 100644 --- a/docs/docs/configuration/genai/config.md +++ b/docs/docs/configuration/genai/config.md @@ -35,18 +35,18 @@ Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger s :::tip -If you are trying to use a single model for Frigate and HomeAssistant, it will need to support vision and tools calling. https://github.com/skye-harris/ollama-modelfiles contains optimized model configs for this task. +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 | -| ----------------- | ----------------------------------------------------------- | -| `Intern3.5VL` | Relatively fast with good vision comprehension -| `gemma3` | Strong frame-to-frame understanding, slower inference times | -| `qwen2.5vl` | Fast but capable model with good vision comprehension | -| `llava-phi3` | Lightweight and fast model with vision comprehension | +| 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 diff --git a/docs/docs/configuration/genai/review_summaries.md b/docs/docs/configuration/genai/review_summaries.md index b630c4da6..4e8107441 100644 --- a/docs/docs/configuration/genai/review_summaries.md +++ b/docs/docs/configuration/genai/review_summaries.md @@ -7,38 +7,95 @@ Generative AI can be used to automatically generate structured summaries of revi 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 with the topic `frigate//review_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_namereviewdescriptionsset). +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: ``` -- `scene` (string): A full description including setting, entities, actions, and any plausible supported inferences. -- `confidence` (float): 0-1 confidence in the analysis. +- `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. +- `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. - -Threat-level definitions: -- 0 — Typical or expected activity for this location/time (includes residents, guests, or known animals engaged in normal activities, even if they glance around or scan surroundings). -- 1 — Unusual or suspicious activity: At least one security-relevant behavior is present **and not explainable by a normal residential activity**. -- 2 — Active or immediate threat: Breaking in, vandalism, aggression, weapon display. ``` -This will show in the UI as a list of concerns that each review item has along with the general description. +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 also automatically show the title and description when the data is available. ### 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. The default `activity_context_prompt` is below: +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 ``` -- **Zone context is critical**: Private enclosed spaces (back yards, back decks, fenced areas, inside garages) are resident territory where brief transient activity, routine tasks, and pet care are expected and normal. Front yards, driveways, and porches are semi-public but still resident spaces where deliveries, parking, and coming/going are routine. Consider whether the zone and activity align with normal residential use. -- **Person + Pet = Normal Activity**: When both "Person" and "Dog" (or "Cat") are detected together in residential zones, this is routine pet care activity (walking, letting out, playing, supervising). Assign Level 0 unless there are OTHER strong suspicious behaviors present (like testing doors, taking items, etc.). A person with their pet in a residential zone is baseline normal activity. -- Brief appearances in private zones (back yards, garages) are normal residential patterns. -- Normal residential activity includes: residents, family members, guests, deliveries, services, maintenance workers, routine property use (parking, unloading, mail pickup, trash removal). -- Brief movement with legitimate items (bags, packages, tools, equipment) in appropriate zones is routine. +### 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: @@ -53,4 +110,4 @@ review: ## Review Reports -Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review. \ No newline at end of file +Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review. diff --git a/docs/docs/configuration/hardware_acceleration_enrichments.md b/docs/docs/configuration/hardware_acceleration_enrichments.md index 84688b8b4..45c7cd4d1 100644 --- a/docs/docs/configuration/hardware_acceleration_enrichments.md +++ b/docs/docs/configuration/hardware_acceleration_enrichments.md @@ -5,7 +5,7 @@ title: Enrichments # Enrichments -Some of Frigate's enrichments can use a discrete GPU / NPU for accelerated processing. +Some of Frigate's enrichments can use a discrete GPU or integrated GPU for accelerated processing. ## Requirements @@ -18,8 +18,10 @@ Object detection and enrichments (like Semantic Search, Face Recognition, and Li - **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. diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index 1b5f6aa29..c1aa62b22 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. 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 @@ -30,7 +30,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 models are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM is required. +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 @@ -74,8 +74,8 @@ Fine-tune the LPR feature using these optional parameters at the global level of - Default: `small` - 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, but uses an enhanced text detector and is more capable at finding characters on multi-line plates. It is significantly slower than the `small` model. Note that using the `large` model does not improve _text recognition_, but it may improve _text detection_. - - For most users, the `small` model is recommended. + - 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 @@ -178,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: @@ -306,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. diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 79802b371..8c310634a 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -174,7 +174,7 @@ For devices that support two way talk, Frigate can be configured to use the feat - Ensure you access Frigate via https (may require [opening port 8971](/frigate/installation/#ports)). - For the Home Assistant Frigate card, [follow the docs](http://card.camera/#/usage/2-way-audio) for the correct source. -To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-doorbell) +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. @@ -214,6 +214,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: 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?** @@ -277,3 +313,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/object_detectors.md b/docs/docs/configuration/object_detectors.md index 7351ef6f4..e7f0bc685 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 @@ -13,8 +15,8 @@ Frigate supports multiple different detectors that work on different types of ha - [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. - [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). +- [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** @@ -34,16 +36,16 @@ 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](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs. @@ -258,41 +260,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 ``` ::: ### 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 @@ -303,6 +319,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. @@ -311,6 +329,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 @@ -331,6 +352,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. @@ -341,6 +364,9 @@ 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. @@ -353,7 +379,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 @@ -367,6 +393,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. @@ -377,6 +405,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 @@ -394,6 +425,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. @@ -404,6 +437,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 @@ -418,15 +454,17 @@ 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`. +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 @@ -614,12 +652,23 @@ detectors: ### 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: #### YOLO-NAS [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. +
+ YOLO-NAS Setup & Config + :::warning If you are using a Frigate+ YOLO-NAS model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. @@ -643,6 +692,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. @@ -653,6 +704,9 @@ 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. @@ -676,12 +730,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 @@ -701,10 +760,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 @@ -721,10 +785,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 @@ -742,6 +811,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) @@ -861,16 +932,16 @@ detectors: model: model_type: yolonas - width: 320 # (Can be set to 640 for higher resolution) - height: 320 # (Can be set to 640 for higher resolution) + 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) + # 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 @@ -889,16 +960,15 @@ detectors: 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) + 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) - # └── yolov9_post.onnx (optional; only if the model includes a cropped post-processing network) + # The .zip file must contain: + # ├── yolov9.dfp (a file ending with .dfp) ``` #### YOLOX @@ -924,8 +994,8 @@ model: 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) + # The .zip file must contain: + # ├── yolox.dfp (a file ending with .dfp) ``` #### SSDLite MobileNet v2 @@ -951,9 +1021,9 @@ model: 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) + # 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 @@ -973,18 +1043,19 @@ To use your own model: 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) +# 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 @@ -1092,16 +1163,16 @@ A synap model is provided in the container at /mobilenet.synap and is used by th 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 +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 +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 @@ -1275,97 +1346,101 @@ Explanation of the paramters: ## 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. +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" + 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 + 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 - + 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 + 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). - + 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 + path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here ``` ## AXERA @@ -1423,7 +1498,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ WORKDIR /dfine RUN git clone https://github.com/Peterande/D-FINE.git . RUN uv pip install --system -r requirements.txt -RUN uv pip install --system onnx onnxruntime onnxsim +RUN uv pip install --system onnx onnxruntime onnxsim onnxscript # Create output directory and download checkpoint RUN mkdir -p output ARG MODEL_SIZE @@ -1447,9 +1522,9 @@ FROM python:3.11 AS build RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ WORKDIR /rfdetr -RUN uv pip install --system rfdetr onnx onnxruntime onnxsim onnx-graphsurgeon +RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnxscript ARG MODEL_SIZE -RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export()" +RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" FROM scratch ARG MODEL_SIZE COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx @@ -1497,7 +1572,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ WORKDIR /yolov9 ADD https://github.com/WongKinYiu/yolov9.git . RUN uv pip install --system -r requirements.txt -RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 +RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 onnxscript ARG MODEL_SIZE ARG IMG_SIZE ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index c12984d18..907bda21e 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -240,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" @@ -427,6 +429,15 @@ review: 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 @@ -628,7 +639,7 @@ 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) @@ -669,20 +680,18 @@ lpr: # 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}" - # Required if enabled: The model to use with the provider. + # 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: @@ -801,6 +810,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 @@ -920,10 +931,13 @@ cameras: 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: none) - threshold: 0.7 + # 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) + # 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 diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 43c421dd3..0ab7a170c 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -24,6 +24,11 @@ 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: @@ -164,4 +169,4 @@ NOTE: The output will need to be passed with two curly braces `{{output}}` go2rtc: streams: stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}} -``` +``` \ No newline at end of file diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md index 9950a3c8a..91f435ff0 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -78,7 +78,7 @@ 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 / NPU hardware, when available. This depends on the Docker build that is used. You can also target a specific device in a multi-GPU installation. +The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU hardware, when available. This depends on the Docker build that is used. You can also target a specific device in a multi-GPU installation. ```yaml semantic_search: @@ -90,7 +90,7 @@ semantic_search: :::info -If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU / NPU 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. @@ -119,7 +119,7 @@ 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. +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. @@ -128,17 +128,20 @@ Triggers are best configured through the Frigate 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** dialog: - - Enter a **Name** for the trigger (e.g., "red_car_alert"). +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 outline for 3 seconds for easy identification. +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 diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md index 025384a8b..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,6 +86,7 @@ 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. diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index 6cce97b3b..8d999fb85 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. @@ -59,7 +61,7 @@ Frigate supports multiple different detectors that work on different types of ha - [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. +- [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 @@ -78,46 +80,32 @@ Frigate supports multiple different detectors that work on different types of ha **Intel** -- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. +- [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. +- [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](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection. ::: -### Synaptics - -- **Synaptics** Default model is **mobilenet** - -| Name | Synaptics SL1680 Inference Time | -| ---------------- | ------------------------------- | -| ssd mobilenet | ~ 25 ms | -| yolov5m | ~ 118 ms | - -### AXERA - -- **AXEngine** Default model is **yolov9** - -| Name | AXERA AX650N/AX8850N Inference Time | -| ---------------- | ----------------------------------- | -| yolov9-tiny | ~ 4 ms | - ### Hailo-8 Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided. @@ -150,6 +138,7 @@ 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) @@ -174,7 +163,8 @@ Inference speeds vary greatly depending on the CPU or GPU used, some known examp | 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 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 | | | @@ -267,7 +257,7 @@ Inference speeds may vary depending on the host platform. The above data was mea ### 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. @@ -288,6 +278,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. @@ -309,3 +308,11 @@ Basically - When you increase the resolution and/or the frame rate of the stream YES! The Coral does not help with decoding video streams. Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://support.video.ibm.com/hc/en-us/articles/18106203580316-Keyframes-InterFrame-Video-Compression). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work. + +### AXERA + +- **AXEngine** Default model is **yolov9** + +| Name | AXERA AX650N/AX8850N Inference Time | +| ---------------- | ----------------------------------- | +| yolov9-tiny | ~ 4 ms | \ No newline at end of file diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 4622f68be..1f2f64c9d 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 @@ -132,7 +132,7 @@ 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 +### 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 @@ -140,10 +140,10 @@ The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVM - Orange Pi 5 Plus/Max - Multi-M.2 PCIe carrier cards -#### Configuration +#### Configuration -#### Installation +#### 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). @@ -154,7 +154,7 @@ Then follow these steps for installing the correct driver/runtime configuration: 3. Run the script with `./user_installation.sh` 4. **Restart your computer** to complete driver installation. -#### Setup +#### Setup To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable` @@ -280,7 +280,7 @@ or add these options to your `docker run` command: ``` --device /dev/synap \ --device /dev/video0 \ ---device /dev/video1 +--device /dev/video1 ``` #### Configuration @@ -340,12 +340,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://coral.ai/docs/m2/get-started/#2a-on-linux - /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 diff --git a/docs/docs/frigate/updating.md b/docs/docs/frigate/updating.md index fdfbf906b..d95ae83c5 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.1**. 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.1). +The current stable version of Frigate is **0.16.2**. 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.2). 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.1` instead of `0.15.2`). For example: + - Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.16.2` instead of `0.15.2`). For example: ```yaml services: frigate: - image: ghcr.io/blakeblackshear/frigate:0.16.1 + image: ghcr.io/blakeblackshear/frigate:0.16.2 ``` - Then pull the image: ```bash - docker pull ghcr.io/blakeblackshear/frigate:0.16.1 + docker pull ghcr.io/blakeblackshear/frigate:0.16.2 ``` - **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.1`, `0.16.1-tensorrt`, or `stable`): + - Pull the image with the appropriate tag (e.g., `0.16.2`, `0.16.2-tensorrt`, or `stable`): ```bash - docker pull ghcr.io/blakeblackshear/frigate:0.16.1 + docker pull ghcr.io/blakeblackshear/frigate:0.16.2 ``` 3. **Start the Container**: diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 0300e1d49..809c8c833 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -159,9 +159,49 @@ 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. When additional objects are detected or when a zone change occurs, it will publish a, `update` message with the same id. When the review activity has ended a final `end` message is published. +Message published for each changed review item. The first message is published when the `detection` or `alert` is initiated. + +An `update` with the same ID will be published when: + +- The severity changes from `detection` to `alert` +- Additional objects are detected +- An object is recognized via face, lpr, etc. + +When the review activity has ended a final `end` message is published. ```json { @@ -301,6 +341,11 @@ 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`. diff --git a/docs/docs/plus/annotating.md b/docs/docs/plus/annotating.md index 102e4a489..dc8e571be 100644 --- a/docs/docs/plus/annotating.md +++ b/docs/docs/plus/annotating.md @@ -42,6 +42,7 @@ Misidentified objects should have a correct label added. For example, if a perso | `w` | Add box | | `d` | Toggle difficult | | `s` | Switch to the next label | +| `Shift + s` | Switch to the previous label | | `tab` | Select next largest box | | `del` | Delete current box | | `esc` | Deselect/Cancel | diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index c0295faae..8ab9d857e 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: [ { @@ -170,7 +170,7 @@ const config: Config = { ], }, ], - copyright: `Copyright © ${new Date().getFullYear()} Blake Blackshear`, + copyright: `Copyright © ${new Date().getFullYear()} Frigate LLC`, }, }, plugins: [ 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/static/img/branding/LICENSE.md b/docs/static/img/branding/LICENSE.md new file mode 100644 index 000000000..4975f03f9 --- /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 LLC 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 LLC. Frigate LLC 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 LLC. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate LLC. + 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) 2025 Frigate LLC. 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/frigate/api/app.py b/frigate/api/app.py index f84190407..3ef054fc0 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -37,7 +37,6 @@ from frigate.stats.prometheus import get_metrics, update_metrics from frigate.util.builtin import ( clean_camera_user_pass, flatten_config_data, - get_tz_modifiers, process_config_query_string, update_yaml_file_bulk, ) @@ -48,6 +47,7 @@ from frigate.util.services import ( restart_frigate, vainfo_hwaccel, ) +from frigate.util.time import get_tz_modifiers from frigate.version import VERSION logger = logging.getLogger(__name__) @@ -179,6 +179,36 @@ def config(request: Request): return JSONResponse(content=config) +@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") def config_raw(): config_file = find_config_file() @@ -387,20 +417,29 @@ def config_set(request: Request, body: AppConfigSetBody): old_config: FrigateConfig = request.app.frigate_config request.app.frigate_config = config - if body.update_topic and body.update_topic.startswith("config/cameras/"): - _, _, camera, field = body.update_topic.split("/") + 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] + 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) - request.app.config_publisher.publish_update( - CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera), - settings, - ) + # Publish None for removal, actual config for add/update + request.app.config_publisher.publisher.publish( + body.update_topic, settings + ) return JSONResponse( content=( @@ -688,7 +727,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)) diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 14fd804f7..1c1371f51 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -35,6 +35,23 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.auth]) +@router.get("/auth/first_time_login") +def first_time_login(request: Request): + """Return whether the admin first-time login help flag is set in config. + + This endpoint is intentionally unauthenticated so the login page can + query it before a user is authenticated. + """ + auth_config = request.app.frigate_config.auth + + return JSONResponse( + content={ + "admin_first_time_login": auth_config.admin_first_time_login + or auth_config.reset_admin_password + } + ) + + class RateLimiter: _limit = "" @@ -515,6 +532,11 @@ def login(request: Request, body: AppPostLoginBody): 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) diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 01da847bc..ef55a283e 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -3,11 +3,17 @@ 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, Request, Response +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 require_role from frigate.api.defs.tags import Tags @@ -199,19 +205,30 @@ def ffprobe(request: Request, paths: str = "", detailed: bool = False): request.app.frigate_config.ffmpeg, path.strip(), detailed=detailed ) - result = { - "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 "" - ), - } + 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"]: @@ -441,3 +458,537 @@ def _extract_fps(r_frame_rate: str) -> float | None: 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 URL-encoded credentials and add only the + # authenticated version. + cred = f"{quote_plus(username)}:{quote_plus(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 URL-encoded credentials for pattern fallback URIs when provided + auth_str = ( + f"{quote_plus(username)}:{quote_plus(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"{quote_plus(username)}:{quote_plus(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 623ceba32..a2aec6898 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 @@ -17,6 +19,8 @@ from frigate.api.auth import require_role from frigate.api.defs.request.classification_body import ( AudioTranscriptionBody, DeleteFaceImagesBody, + GenerateObjectExamplesBody, + GenerateStateExamplesBody, RenameFaceBody, ) from frigate.api.defs.response.classification_response import ( @@ -27,10 +31,16 @@ 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 CLIPS_DIR, FACE_DIR +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, +) +from frigate.util.file import get_event_snapshot logger = logging.getLogger(__name__) @@ -104,9 +114,18 @@ 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, ) @@ -159,8 +178,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)) @@ -548,23 +566,59 @@ def get_classification_dataset(name: 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={}) + return JSONResponse( + status_code=200, content={"categories": {}, "training_metadata": None} + ) - for name in os.listdir(dataset_dir): - category_dir = os.path.join(dataset_dir, name) + 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[name] = [] + dataset_dict[category_name] = [] for file in filter( lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))), os.listdir(category_dir), ): - dataset_dict[name].append(file) + dataset_dict[category_name].append(file) - return JSONResponse(status_code=200, content=dataset_dict) + # 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( @@ -655,12 +709,106 @@ def delete_classification_dataset_images( if os.path.isfile(file_path): os.unlink(file_path) + if os.path.exists(folder) and not os.listdir(folder): + os.rmdir(folder) + return JSONResponse( - content=({"success": True, "message": "Successfully deleted faces."}), + 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) + 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, @@ -701,13 +849,14 @@ def categorize_classification_image(request: Request, name: str, body: dict = No status_code=404, ) - new_name = f"{category}-{datetime.datetime.now().timestamp()}.png" + 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 ) - if not os.path.exists(new_file_folder): - os.mkdir(new_file_folder) + os.makedirs(new_file_folder, exist_ok=True) # use opencv because webp images can not be used to train img = cv2.imread(training_file) @@ -715,7 +864,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No os.unlink(training_file) return JSONResponse( - content=({"success": True, "message": "Successfully deleted faces."}), + content=({"success": True, "message": "Successfully categorized image."}), status_code=200, ) @@ -753,6 +902,87 @@ def delete_classification_train_images(request: Request, name: str, body: dict = os.unlink(file_path) return JSONResponse( - content=({"success": True, "message": "Successfully deleted faces."}), + 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/request/classification_body.py b/frigate/api/defs/request/classification_body.py index dabff0912..fb6a7dd0f 100644 --- a/frigate/api/defs/request/classification_body.py +++ b/frigate/api/defs/request/classification_body.py @@ -1,17 +1,31 @@ -from typing import List +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 + 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/event.py b/frigate/api/event.py index a8b016252..13886af13 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -2,6 +2,7 @@ import base64 import datetime +import json import logging import os import random @@ -57,8 +58,8 @@ from frigate.const import CLIPS_DIR, TRIGGER_DIR from frigate.embeddings import EmbeddingsContext from frigate.models import Event, ReviewSegment, Timeline, Trigger from frigate.track.object_processing import TrackedObject -from frigate.util.builtin import get_tz_modifiers -from frigate.util.path import get_event_thumbnail_bytes +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.time import get_dst_transitions, get_tz_modifiers logger = logging.getLogger(__name__) @@ -813,7 +814,6 @@ def events_summary( 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 @@ -828,33 +828,91 @@ def events_summary( 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"), + 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)) - .group_by( - Event.camera, - Event.label, - Event.sub_label, - Event.data, - (Event.start_time + seconds_offset).cast("int") / (3600 * 24), - Event.zones, - ) + .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( @@ -1723,9 +1781,8 @@ def create_trigger_embedding( logger.debug( f"Writing thumbnail for trigger with data {body.data} in {camera_name}." ) - except Exception as e: - logger.error(e.with_traceback()) - logger.error( + except Exception: + logger.exception( f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" ) @@ -1749,8 +1806,8 @@ def create_trigger_embedding( status_code=200, ) - except Exception as e: - logger.error(e.with_traceback()) + except Exception: + logger.exception("Error creating trigger embedding") return JSONResponse( content={ "success": False, @@ -1859,9 +1916,8 @@ def update_trigger_embedding( logger.debug( f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." ) - except Exception as e: - logger.error(e.with_traceback()) - logger.error( + except Exception: + logger.exception( f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" ) @@ -1900,9 +1956,8 @@ def update_trigger_embedding( logger.debug( f"Writing thumbnail for trigger with data {body.data} in {camera_name}." ) - except Exception as e: - logger.error(e.with_traceback()) - logger.error( + except Exception: + logger.exception( f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" ) @@ -1914,8 +1969,8 @@ def update_trigger_embedding( status_code=200, ) - except Exception as e: - logger.error(e.with_traceback()) + except Exception: + logger.exception("Error updating trigger embedding") return JSONResponse( content={ "success": False, @@ -1975,9 +2030,8 @@ def delete_trigger_embedding( logger.debug( f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." ) - except Exception as e: - logger.error(e.with_traceback()) - logger.error( + except Exception: + logger.exception( f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" ) @@ -1989,8 +2043,8 @@ def delete_trigger_embedding( status_code=200, ) - except Exception as e: - logger.error(e.with_traceback()) + except Exception: + logger.exception("Error deleting trigger embedding") return JSONResponse( content={ "success": False, diff --git a/frigate/api/export.py b/frigate/api/export.py index 2fbb891c2..d7b314ab2 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -9,6 +9,7 @@ from typing import List import psutil from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filepath from peewee import DoesNotExist from playhouse.shortcuts import model_to_dict @@ -26,14 +27,14 @@ from frigate.api.defs.response.export_response import ( ) from frigate.api.defs.response.generic_response import GenericResponse from frigate.api.defs.tags import Tags -from frigate.const import EXPORT_DIR +from frigate.const import CLIPS_DIR, EXPORT_DIR from frigate.models import Export, Previews, Recordings from frigate.record.export import ( PlaybackFactorEnum, PlaybackSourceEnum, RecordingExporter, ) -from frigate.util.builtin import is_current_hour +from frigate.util.time import is_current_hour logger = logging.getLogger(__name__) @@ -88,7 +89,14 @@ def export_recording( playback_factor = body.playback playback_source = body.source friendly_name = body.name - existing_image = body.image_path + existing_image = sanitize_filepath(body.image_path) if body.image_path else None + + # Ensure that existing_image is a valid path + if existing_image and not existing_image.startswith(CLIPS_DIR): + return JSONResponse( + content=({"success": False, "message": "Invalid image path"}), + status_code=400, + ) if playback_source == "recordings": recordings_count = ( diff --git a/frigate/api/media.py b/frigate/api/media.py index 87456978e..372404b5a 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -44,9 +44,9 @@ 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__) @@ -424,7 +424,6 @@ def all_recordings_summary( 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": @@ -432,43 +431,72 @@ def all_recordings_summary( filtered = requested.intersection(allowed_cameras) if not filtered: return JSONResponse(content={}) - cameras = ",".join(filtered) + camera_list = list(filtered) else: - cameras = allowed_cameras + 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 params.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( @@ -476,61 +504,103 @@ def all_recordings_summary( ) 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())) @@ -589,7 +659,7 @@ async def no_recordings( ) scale = params.scale - clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] + clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)] if cameras != "all": camera_list = cameras.split(",") clauses.append((Recordings.camera << camera_list)) @@ -608,33 +678,39 @@ async def no_recordings( # Convert recordings to list of (start, end) tuples recordings = [(r["start_time"], r["end_time"]) for r in data] - # Generate all time segments - current = after + # Iterate through time segments and check if each has any recording no_recording_segments = [] - current_start = None + current = after + current_gap_start = None while current < before: - segment_end = current + scale - # Check if segment overlaps with any recording + segment_end = min(current + scale, before) + + # Check if this segment overlaps with any recording has_recording = any( - start <= segment_end and end >= current for start, end in recordings + rec_start < segment_end and rec_end > current + for rec_start, rec_end in recordings ) + if not has_recording: - if current_start is None: - current_start = current # Start a new gap + # This segment has no recordings + if current_gap_start is None: + current_gap_start = current # Start a new gap else: - if current_start is not None: + # 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_start), "end_time": int(current)} + {"start_time": int(current_gap_start), "end_time": int(current)} ) - current_start = None + current_gap_start = None + current = segment_end # Append the last gap if it exists - if current_start is not None: + if current_gap_start is not None: no_recording_segments.append( - {"start_time": int(current_start), "end_time": int(before)} + {"start_time": int(current_gap_start), "end_time": int(before)} ) return JSONResponse(content=no_recording_segments) @@ -686,6 +762,15 @@ async 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: @@ -764,6 +849,7 @@ async 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 @@ -781,11 +867,11 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float): 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) continue - if 0 < duration < max_duration_ms: + if min_duration_ms <= duration < max_duration_ms: clip["keyFrameDurations"] = [duration] clips.append(clip) durations.append(duration) diff --git a/frigate/api/review.py b/frigate/api/review.py index efb2269a7..300255663 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -36,7 +36,7 @@ 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__) @@ -197,7 +197,6 @@ async def review_summary( 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 @@ -329,89 +328,135 @@ 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) diff --git a/frigate/app.py b/frigate/app.py index 858247866..30259ad3d 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -488,6 +488,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. ***") diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index 865fe4725..815e650e9 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -136,6 +136,7 @@ class CameraMaintainer(threading.Thread): self.ptz_metrics[name], self.region_grids[name], self.stop_event, + self.config.logger, ) self.camera_processes[config.name] = camera_process camera_process.start() @@ -156,7 +157,11 @@ class CameraMaintainer(threading.Thread): self.frame_manager.create(f"{config.name}_frame{i}", frame_size) capture_process = CameraCapture( - config, count, self.camera_metrics[name], self.stop_event + config, + count, + self.camera_metrics[name], + self.stop_event, + self.config.logger, ) capture_process.daemon = True self.capture_processes[name] = capture_process diff --git a/frigate/config/auth.py b/frigate/config/auth.py index fd5d0e394..fced20620 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -38,6 +38,13 @@ class AuthConfig(FrigateBaseModel): 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 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 967a69427..0f2b1c8be 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -177,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 diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py index 925a91de2..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"] @@ -77,6 +85,10 @@ class GenAIReviewConfig(FrigateBaseModel): ) 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.", @@ -93,13 +105,40 @@ class GenAIReviewConfig(FrigateBaseModel): default=None, ) activity_context_prompt: str = Field( - default="""- **Zone context is critical**: Private enclosed spaces (back yards, back decks, fenced areas, inside garages) are resident territory where brief transient activity, routine tasks, and pet care are expected and normal. Front yards, driveways, and porches are semi-public but still resident spaces where deliveries, parking, and coming/going are routine. Consider whether the zone and activity align with normal residential use. -- **Person + Pet = Normal Activity**: When both "Person" and "Dog" (or "Cat") are detected together in residential zones, this is routine pet care activity (walking, letting out, playing, supervising). Assign Level 0 unless there are OTHER strong suspicious behaviors present (like testing doors, taking items, etc.). A person with their pet in a residential zone is baseline normal activity. -- Brief appearances in private zones (back yards, garages) are normal residential patterns. -- Normal residential activity includes: residents, family members, guests, deliveries, services, maintenance workers, routine property use (parking, unloading, mail pickup, trash removal). -- Brief movement with legitimate items (bags, packages, tools, equipment) in appropriate zones is routine. -""", - title="Custom activity context prompt defining normal activity patterns for this property.", + 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.", ) 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 56126e4d4..bdcbf48f1 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -33,6 +33,8 @@ class TriggerType(str, Enum): class TriggerAction(str, Enum): NOTIFICATION = "notification" + SUB_LABEL = "sub_label" + ATTRIBUTE = "attribute" class ObjectClassificationType(str, Enum): @@ -69,7 +71,7 @@ class BirdClassificationConfig(FrigateBaseModel): class CustomClassificationStateCameraConfig(FrigateBaseModel): - crop: list[int, int, int, int] = Field( + crop: list[float, float, float, float] = Field( title="Crop of image frame on this camera to run classification on." ) @@ -197,7 +199,9 @@ 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." diff --git a/frigate/config/config.py b/frigate/config/config.py index 7ce9c73b4..6342c13bf 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -792,6 +792,10 @@ class FrigateConfig(FrigateBaseModel): # 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. diff --git a/frigate/data_processing/common/audio_transcription/model.py b/frigate/data_processing/common/audio_transcription/model.py index 0fe5ddb5c..82472ad62 100644 --- a/frigate/data_processing/common/audio_transcription/model.py +++ b/frigate/data_processing/common/audio_transcription/model.py @@ -4,7 +4,6 @@ import logging import os import sherpa_onnx -from faster_whisper.utils import download_model from frigate.comms.inter_process import InterProcessRequestor from frigate.const import MODEL_CACHE_DIR @@ -25,6 +24,9 @@ class AudioTranscriptionModelRunner: 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", diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index 80a169c25..a2509d4fa 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -14,8 +14,8 @@ 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 ( @@ -1123,7 +1123,9 @@ class LicensePlateProcessingMixin: for i, plate in enumerate(plates): merged = False for j, cluster in enumerate(clusters): - sims = [jaro_winkler(plate["plate"], v["plate"]) for v in cluster] + 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: @@ -1500,7 +1502,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( @@ -1580,7 +1582,8 @@ class LicensePlateProcessingMixin: for label, plates_list in self.lpr_config.known_plates.items() if any( re.match(f"^{plate}$", rep_plate) - or distance(plate, rep_plate) <= self.lpr_config.match_distance + or Levenshtein.distance(plate, rep_plate) + <= self.lpr_config.match_distance for plate in plates_list ) ), diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py index 146b4e0f1..870c34068 100644 --- a/frigate/data_processing/post/audio_transcription.py +++ b/frigate/data_processing/post/audio_transcription.py @@ -6,10 +6,8 @@ import threading import time from typing import Optional -from faster_whisper import WhisperModel from peewee import DoesNotExist -from frigate.comms.embeddings_updater import EmbeddingsRequestEnum from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig from frigate.const import ( @@ -32,11 +30,13 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): 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 @@ -50,6 +50,9 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): 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" @@ -128,10 +131,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): ) # Embed the description - self.requestor.send_data( - EmbeddingsRequestEnum.embed_description.value, - {"id": event_id, "description": transcription}, - ) + self.embeddings.embed_description(event_id, transcription) except DoesNotExist: logger.debug("No recording found for audio transcription post-processing") diff --git a/frigate/data_processing/post/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py index 23af43548..1f4608bc3 100644 --- a/frigate/data_processing/post/object_descriptions.py +++ b/frigate/data_processing/post/object_descriptions.py @@ -20,8 +20,8 @@ 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 -from frigate.util.path import get_event_thumbnail_bytes if TYPE_CHECKING: from frigate.embeddings import Embeddings diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py index 69110564c..fadc483c3 100644 --- a/frigate/data_processing/post/review_descriptions.py +++ b/frigate/data_processing/post/review_descriptions.py @@ -3,6 +3,7 @@ import copy import datetime import logging +import math import os import shutil import threading @@ -10,22 +11,28 @@ from pathlib import Path from typing import Any import cv2 +from peewee import DoesNotExist from frigate.comms.embeddings_updater import EmbeddingsRequestEnum from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig -from frigate.config.camera.review import GenAIReviewConfig +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 ReviewSegment +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__( @@ -43,20 +50,53 @@ class ReviewDescriptionProcessor(PostProcessorApi): self.review_descs_dps = EventsPerSecond() self.review_descs_dps.start() - def calculate_frame_count(self) -> int: - """Calculate optimal number of frames based on context size.""" - # With our preview images (height of 180px) each image should be ~100 tokens per image - # We want to be conservative to not have too long of query times with too many images - context_size = self.genai_client.get_context_size() + 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. - if context_size > 10000: - return 20 - elif context_size > 6000: - return 16 - elif context_size > 4000: - return 12 + 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: - return 8 + 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 = 3500 + 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() @@ -88,36 +128,61 @@ class ReviewDescriptionProcessor(PostProcessorApi): ): return - frames = self.get_cache_frames( - camera, final_data["start_time"], final_data["end_time"] - ) + image_source = camera_config.review.genai.image_source - if not frames: - frames = [final_data["thumb_path"]] + if image_source == ImageSourceEnum.recordings: + duration = final_data["end_time"] - final_data["start_time"] + buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT) - thumbs = [] + # 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) - for idx, thumb_path in enumerate(frames): - thumb_data = cv2.imread(thumb_path) - ret, jpg = cv2.imencode( - ".jpg", thumb_data, [int(cv2.IMWRITE_JPEG_QUALITY), 100] + 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 ret: - thumbs.append(jpg.tobytes()) - - if camera_config.review.genai.debug_save_thumbnails: - id = data["after"]["id"] - Path(os.path.join(CLIPS_DIR, "genai-requests", f"{id}")).mkdir( + 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 ) - shutil.copy( - thumb_path, - os.path.join( - CLIPS_DIR, - f"genai-requests/{id}/{idx}.webp", - ), - ) + 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() @@ -127,11 +192,12 @@ class ReviewDescriptionProcessor(PostProcessorApi): self.requestor, self.genai_client, self.review_desc_speed, - camera, + camera_config, final_data, thumbs, camera_config.review.genai, list(self.config.model.merged_labelmap.values()), + self.config.model.all_attributes, ), ).start() @@ -217,7 +283,7 @@ class ReviewDescriptionProcessor(PostProcessorApi): all_frames.append(os.path.join(preview_dir, file)) frame_count = len(all_frames) - desired_frame_count = self.calculate_frame_count() + desired_frame_count = self.calculate_frame_count(camera) if frame_count <= desired_frame_count: return all_frames @@ -231,48 +297,179 @@ class ReviewDescriptionProcessor(PostProcessorApi): 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: str, + 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, - "zones": final_data["data"]["zones"], + "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"]), } - objects = [] - named_objects = [] + 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 = sub_labels_list[i].replace("_", " ").title() + unified_objects.append(f"{name} ({object_type})") + for label in objects_list: if "-verified" in label: continue elif label in labelmap_objects: - objects.append(label.replace("_", " ").title()) + object_type = label.replace("_", " ").title() - for i, verified_label in enumerate(final_data["data"]["verified_objects"]): - named_objects.append( - f"{sub_labels_list[i].replace('_', ' ').title()} ({verified_label.replace('-verified', '')})" - ) + if label in attribute_labels: + unified_objects.append(f"{object_type} (delivery/service)") + else: + unified_objects.append(object_type) - analytics_data["objects"] = objects - analytics_data["recognized_objects"] = named_objects + analytics_data["unified_objects"] = unified_objects metadata = genai_client.generate_review_description( analytics_data, diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index db1aff751..ec9e5d220 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -10,6 +10,10 @@ 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 @@ -18,7 +22,7 @@ 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.path import get_event_thumbnail_bytes +from frigate.util.file import get_event_thumbnail_bytes from ..post.api import PostProcessorApi from ..types import DataProcessorMetrics @@ -34,6 +38,7 @@ class SemanticTriggerProcessor(PostProcessorApi): db: SqliteVecQueueDatabase, config: FrigateConfig, requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, metrics: DataProcessorMetrics, embeddings, ): @@ -41,6 +46,7 @@ class SemanticTriggerProcessor(PostProcessorApi): 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() @@ -184,14 +190,44 @@ class SemanticTriggerProcessor(PostProcessorApi): ), ) + friendly_name = ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .friendly_name + ) + if ( self.config.cameras[camera] .semantic_search.triggers[trigger["name"]] .actions ): - # TODO: handle actions for the trigger + # handle actions for the trigger # notifications already handled by webpush - pass + 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: diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py index e5e4fc90e..b5e8b1e35 100644 --- a/frigate/data_processing/real_time/custom_classification.py +++ b/frigate/data_processing/real_time/custom_classification.py @@ -1,6 +1,7 @@ """Real time processor that works with classification tflite models.""" import datetime +import json import logging import os from typing import Any @@ -21,6 +22,7 @@ from frigate.config.classification import ( ) from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR from frigate.log import redirect_output_to_logger +from frigate.types import TrackedObjectUpdateTypesEnum from frigate.util.builtin import EventsPerSecond, InferenceSpeed, load_labels from frigate.util.object import box_overlaps, calculate_region @@ -34,6 +36,8 @@ except ModuleNotFoundError: logger = logging.getLogger(__name__) +MAX_OBJECT_CLASSIFICATIONS = 16 + class CustomStateClassificationProcessor(RealTimeProcessorApi): def __init__( @@ -53,9 +57,18 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): self.tensor_output_details: dict[str, Any] | None = None self.labelmap: dict[int, str] = {} self.classifications_per_second = EventsPerSecond() - self.inference_speed = InferenceSpeed( - self.metrics.classification_speeds[self.model_config.name] - ) + 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() @@ -83,12 +96,50 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): def __update_metrics(self, duration: float) -> None: self.classifications_per_second.update() - self.inference_speed.update(duration) + if self.inference_speed: + self.inference_speed.update(duration) + + 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): - self.metrics.classification_cps[ - self.model_config.name - ].value = self.classifications_per_second.eps() + 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: @@ -96,10 +147,10 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): camera_config = self.model_config.state_config.cameras[camera] crop = [ - camera_config.crop[0], - camera_config.crop[1], - camera_config.crop[2], - camera_config.crop[3], + 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 @@ -121,6 +172,19 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): 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 @@ -165,6 +229,9 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): 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) @@ -178,10 +245,19 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): score, ) - if score >= self.model_config.threshold: + if score < self.model_config.threshold: + logger.debug( + f"Score {score} below threshold {self.model_config.threshold}, skipping verification" + ) + return + + detected_state = self.labelmap[best_id] + 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}", - self.labelmap[best_id], + verified_state, ) def handle_request(self, topic, request_data): @@ -210,6 +286,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): config: FrigateConfig, model_config: CustomClassificationConfig, sub_label_publisher: EventMetadataPublisher, + requestor: InterProcessRequestor, metrics: DataProcessorMetrics, ): super().__init__(config, metrics) @@ -218,14 +295,23 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): self.train_dir = os.path.join(CLIPS_DIR, self.model_config.name, "train") self.interpreter: Interpreter | None = None self.sub_label_publisher = sub_label_publisher + self.requestor = requestor self.tensor_input_details: dict[str, Any] | None = None self.tensor_output_details: dict[str, Any] | None = None - self.detected_objects: dict[str, float] = {} + self.classification_history: dict[str, list[tuple[str, float, float]]] = {} self.labelmap: dict[int, str] = {} self.classifications_per_second = EventsPerSecond() - self.inference_speed = InferenceSpeed( - self.metrics.classification_speeds[self.model_config.name] - ) + + 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() @redirect_output_to_logger(logger, logging.DEBUG) @@ -251,12 +337,64 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): def __update_metrics(self, duration: float) -> None: self.classifications_per_second.update() - self.inference_speed.update(duration) + 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): - self.metrics.classification_cps[ - self.model_config.name - ].value = self.classifications_per_second.eps() + 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 @@ -264,6 +402,21 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): if obj_data["label"] not in self.model_config.object_config.objects: return + if obj_data.get("end_time") is not None: + return + + if obj_data.get("stationary"): + 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, @@ -272,8 +425,8 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): obj_data["box"][2], obj_data["box"][3], max( - obj_data["box"][1] - obj_data["box"][0], - obj_data["box"][3] - obj_data["box"][2], + obj_data["box"][2] - obj_data["box"][0], + obj_data["box"][3] - obj_data["box"][1], ), 1.0, ) @@ -295,7 +448,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): write_classification_attempt( self.train_dir, cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), - obj_data["id"], + object_id, now, "unknown", 0.0, @@ -309,48 +462,85 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): 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) - previous_score = self.detected_objects.get(obj_data["id"], 0.0) self.__update_metrics(datetime.datetime.now().timestamp() - now) write_classification_attempt( self.train_dir, cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), - obj_data["id"], + object_id, now, self.labelmap[best_id], score, + max_files=200, ) if score < self.model_config.threshold: logger.debug(f"Score {score} is less than threshold.") return - if score <= previous_score: - logger.debug(f"Score {score} is worse than previous score {previous_score}") - return - sub_label = self.labelmap[best_id] - self.detected_objects[obj_data["id"]] = score - if ( - self.model_config.object_config.classification_type - == ObjectClassificationType.sub_label - ): - if sub_label != "none": + 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( - (obj_data["id"], sub_label, score), + (object_id, consensus_label, consensus_score), EventMetadataTypeEnum.sub_label, ) - elif ( - self.model_config.object_config.classification_type - == ObjectClassificationType.attribute - ): - self.sub_label_publisher.publish( - (obj_data["id"], self.model_config.name, sub_label, 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, + "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: @@ -368,8 +558,8 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): return None def expire_object(self, object_id, camera): - if object_id in self.detected_objects: - self.detected_objects.pop(object_id) + if object_id in self.classification_history: + self.classification_history.pop(object_id) @staticmethod @@ -380,6 +570,7 @@ def write_classification_attempt( timestamp: float, label: str, score: float, + max_files: int = 100, ) -> None: if "-" in label: label = label.replace("-", "_") @@ -395,5 +586,8 @@ def write_classification_attempt( ) # delete oldest face image if maximum is reached - if len(files) > 100: - os.unlink(os.path.join(folder, files[-1])) + try: + if len(files) > max_files: + os.unlink(os.path.join(folder, files[-1])) + except FileNotFoundError: + pass diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index d3ebc5397..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() @@ -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 @@ -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/detectors/detection_runners.py b/frigate/detectors/detection_runners.py index c1bc98c6c..eb3a0ecb9 100644 --- a/frigate/detectors/detection_runners.py +++ b/frigate/detectors/detection_runners.py @@ -3,6 +3,7 @@ import logging import os import platform +import threading from abc import ABC, abstractmethod from typing import Any @@ -21,21 +22,25 @@ def is_arm64_platform() -> bool: return machine in ("aarch64", "arm64", "armv8", "armv7l") -def get_ort_session_options() -> ort.SessionOptions | None: +def get_ort_session_options( + is_complex_model: bool = False, +) -> ort.SessionOptions | None: """Get ONNX Runtime session options with appropriate settings. - On ARM/RKNN platforms, use basic optimizations to avoid graph fusion issues - that can break certain models. On amd64, use default optimizations for better performance. - """ - sess_options = None + Args: + is_complex_model: Whether the model needs basic optimization to avoid graph fusion issues. - if is_arm64_platform(): + 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 sess_options + return None # Import OpenVINO only when needed to avoid circular dependencies @@ -103,6 +108,21 @@ class BaseModelRunner(ABC): 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 @@ -142,12 +162,12 @@ class CudaGraphRunner(BaseModelRunner): """ @staticmethod - def is_complex_model(model_type: str) -> bool: + 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 in [ + return model_type not in [ ModelTypeEnum.yolonas.value, EnrichmentModelTypeEnum.paddleocr.value, EnrichmentModelTypeEnum.jina_v1.value, @@ -215,11 +235,36 @@ class OpenVINOModelRunner(BaseModelRunner): # Import here to avoid circular imports from frigate.embeddings.types import EnrichmentModelTypeEnum - return model_type in [EnrichmentModelTypeEnum.paddleocr.value] + 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): @@ -247,6 +292,10 @@ class OpenVINOModelRunner(BaseModelRunner): 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() @@ -290,55 +339,81 @@ class OpenVINOModelRunner(BaseModelRunner): Returns: List of output tensors """ - # 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: + # 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: - # 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 + self.infer_request.infer() + except Exception as e: + logger.error(f"Error during OpenVINO inference: {e}") + return [] - # Multiple inputs case - set each input by name - for input_name, input_data in inputs.items(): - # Find the input by name - input_port = None - for port in self.compiled_model.inputs: - if port.get_any_name() == input_name: - input_port = port - break + # Get all output tensors + outputs = [] + for i in range(len(self.compiled_model.outputs)): + outputs.append(self.infer_request.get_output_tensor(i).data) - 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() - input_tensor = ov.Tensor(input_element_type, input_data.shape) - np.copyto(input_tensor.data, input_data) - - # Set the input tensor - self.infer_request.set_input_tensor(input_tensor) - - # Run inference - self.infer_request.infer() - - # 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 + return outputs class RKNNModelRunner(BaseModelRunner): @@ -466,7 +541,7 @@ def get_optimized_runner( return OpenVINOModelRunner(model_path, device, model_type, **kwargs) if ( - not CudaGraphRunner.is_complex_model(model_type) + CudaGraphRunner.is_model_supported(model_type) and providers[0] == "CUDAExecutionProvider" ): options[0] = { @@ -494,7 +569,9 @@ def get_optimized_runner( return ONNXModelRunner( ort.InferenceSession( model_path, - sess_options=get_ort_session_options(), + sess_options=get_ort_session_options( + ONNXModelRunner.is_cpu_complex_model(model_type) + ), providers=providers, provider_options=options, ) diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py index 3b424bcc0..f4977acad 100644 --- a/frigate/detectors/plugins/memryx.py +++ b/frigate/detectors/plugins/memryx.py @@ -17,7 +17,7 @@ from frigate.detectors.detector_config import ( BaseDetectorConfig, ModelTypeEnum, ) -from frigate.util.model import post_process_yolo +from frigate.util.file import FileLock logger = logging.getLogger(__name__) @@ -177,44 +177,14 @@ class MemryXDetector(DetectionApi): logger.error(f"Failed to initialize MemryX model: {e}") raise - def _acquire_file_lock(self, lock_path: str, timeout: int = 60, poll: float = 0.2): - """ - Create an exclusive lock file. Blocks (with polling) until it can acquire, - or raises TimeoutError. Uses only stdlib (os.O_EXCL). - """ - start = time.time() - while True: - try: - fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_RDWR) - os.close(fd) - return - except FileExistsError: - if time.time() - start > timeout: - raise TimeoutError(f"Timeout waiting for lock: {lock_path}") - time.sleep(poll) - - def _release_file_lock(self, lock_path: str): - """Best-effort removal of the lock file.""" - try: - os.remove(lock_path) - except FileNotFoundError: - pass - - def load_yolo_constants(self): - base = f"{self.cache_dir}/{self.model_folder}" - # constants for yolov9 post-processing - self.const_A = np.load(f"{base}/_model_22_Constant_9_output_0.npy") - self.const_B = np.load(f"{base}/_model_22_Constant_10_output_0.npy") - self.const_C = np.load(f"{base}/_model_22_Constant_12_output_0.npy") - 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") - self._acquire_file_lock(lock_path) + lock = FileLock(lock_path, timeout=60) - try: + with lock: # ---------- CASE 1: user provided a custom model path ---------- if self.memx_model_path: if not self.memx_model_path.endswith(".zip"): @@ -258,7 +228,6 @@ class MemryXDetector(DetectionApi): # Handle post model requirements by model type if self.memx_model_type in [ - ModelTypeEnum.yologeneric, ModelTypeEnum.yolonas, ModelTypeEnum.ssd, ]: @@ -267,7 +236,10 @@ class MemryXDetector(DetectionApi): 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 == ModelTypeEnum.yolox: + elif self.memx_model_type in [ + ModelTypeEnum.yolox, + ModelTypeEnum.yologeneric, + ]: # Explicitly ignore any post model even if present self.memx_post_model = None else: @@ -295,8 +267,6 @@ class MemryXDetector(DetectionApi): logger.info("Using cached models.") self.memx_model_path = dfp_path self.memx_post_model = post_path - if self.memx_model_type == ModelTypeEnum.yologeneric: - self.load_yolo_constants() return # ---------- CASE 3: download MemryX model (no cache) ---------- @@ -325,9 +295,6 @@ class MemryXDetector(DetectionApi): else None ) - if self.memx_model_type == ModelTypeEnum.yologeneric: - self.load_yolo_constants() - finally: if os.path.exists(zip_path): try: @@ -338,9 +305,6 @@ class MemryXDetector(DetectionApi): f"Failed to remove downloaded zip {zip_path}: {e}" ) - finally: - self._release_file_lock(lock_path) - 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: @@ -625,127 +589,232 @@ class MemryXDetector(DetectionApi): self.output_queue.put(final_detections) - def onnx_reshape_with_allowzero( - self, data: np.ndarray, shape: np.ndarray, allowzero: int = 0 + 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: - shape = shape.astype(int) - input_shape = data.shape - output_shape = [] + """Convert distances to bounding boxes - YOLOv9 style""" + lt = x[:, :2] + rb = x[:, 2:] - for i, dim in enumerate(shape): - if dim == 0 and allowzero == 0: - output_shape.append(input_shape[i]) # Copy dimension from input - else: - output_shape.append(dim) + x1y1 = anchors - lt + x2y2 = anchors + rb - # Now let NumPy infer any -1 if needed - reshaped = np.reshape(data, output_shape) + wh = x2y2 - x1y1 + c_xy = (x1y1 + x2y2) / 2 - return reshaped + 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: - if not self.memx_post_model: - conv_out1 = outputs[0] - conv_out2 = outputs[1] - conv_out3 = outputs[2] - conv_out4 = outputs[3] - conv_out5 = outputs[4] - conv_out6 = outputs[5] + # Use complete YOLOv9-style postprocessing (includes NMS) + final_detections = self.post_process_yolo_optimized(outputs) - concat_1 = self.onnx_concat([conv_out1, conv_out2], axis=1) - concat_2 = self.onnx_concat([conv_out3, conv_out4], axis=1) - concat_3 = self.onnx_concat([conv_out5, conv_out6], axis=1) - - shape = np.array([1, 144, -1], dtype=np.int64) - - reshaped_1 = self.onnx_reshape_with_allowzero( - concat_1, shape, allowzero=0 - ) - reshaped_2 = self.onnx_reshape_with_allowzero( - concat_2, shape, allowzero=0 - ) - reshaped_3 = self.onnx_reshape_with_allowzero( - concat_3, shape, allowzero=0 - ) - - concat_4 = self.onnx_concat([reshaped_1, reshaped_2, reshaped_3], 2) - - axis = 1 - split_sizes = [64, 80] - - # Calculate indices at which to split - indices = np.cumsum(split_sizes)[ - :-1 - ] # [64] — split before the second chunk - - # Perform split along axis 1 - split_0, split_1 = np.split(concat_4, indices, axis=axis) - - num_boxes = 2100 if self.memx_model_height == 320 else 8400 - shape1 = np.array([1, 4, 16, num_boxes]) - reshape_4 = self.onnx_reshape_with_allowzero( - split_0, shape1, allowzero=0 - ) - - transpose_1 = reshape_4.transpose(0, 2, 1, 3) - - axis = 1 # As per ONNX softmax node - - # Subtract max for numerical stability - x_max = np.max(transpose_1, axis=axis, keepdims=True) - x_exp = np.exp(transpose_1 - x_max) - x_sum = np.sum(x_exp, axis=axis, keepdims=True) - softmax_output = x_exp / x_sum - - # Weight W from the ONNX initializer (1, 16, 1, 1) with values 0 to 15 - W = np.arange(16, dtype=np.float32).reshape( - 1, 16, 1, 1 - ) # (1, 16, 1, 1) - - # Apply 1x1 convolution: this is a weighted sum over channels - conv_output = np.sum( - softmax_output * W, axis=1, keepdims=True - ) # shape: (1, 1, 4, 8400) - - shape2 = np.array([1, 4, num_boxes]) - reshape_5 = self.onnx_reshape_with_allowzero( - conv_output, shape2, allowzero=0 - ) - - # ONNX Slice — get first 2 channels: [0:2] along axis 1 - slice_output1 = reshape_5[:, 0:2, :] # Result: (1, 2, 8400) - - # Slice channels 2 to 4 → axis = 1 - slice_output2 = reshape_5[:, 2:4, :] - - # Perform Subtraction - sub_output = self.const_A - slice_output1 # Equivalent to ONNX Sub - - # Perform the ONNX-style Add - add_output = self.const_B + slice_output2 - - sub1 = add_output - sub_output - - add1 = sub_output + add_output - - div_output = add1 / 2.0 - - concat_5 = self.onnx_concat([div_output, sub1], axis=1) - - # Expand B to (1, 1, 8400) so it can broadcast across axis=1 (4 channels) - const_C_expanded = self.const_C[:, np.newaxis, :] # Shape: (1, 1, 8400) - - # Perform ONNX-style element-wise multiplication - mul_output = concat_5 * const_C_expanded # Result: (1, 4, 8400) - - sigmoid_output = self.sigmoid(split_1) - outputs = self.onnx_concat([mul_output, sigmoid_output], axis=1) - - final_detections = post_process_yolo( - outputs, self.memx_model_width, self.memx_model_height - ) self.output_queue.put(final_detections) elif self.memx_model_type == ModelTypeEnum.yolonas: diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py index 788e3e6db..01d011ae2 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -29,7 +29,7 @@ from frigate.db.sqlitevecq import SqliteVecQueueDatabase from frigate.models import Event, Trigger from frigate.types import ModelStatusTypesEnum from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize -from frigate.util.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 @@ -472,7 +472,7 @@ class Embeddings: ) thumbnail_missing = True except DoesNotExist: - logger.warning( + logger.debug( f"Event ID {trigger.data} for trigger {trigger_name} does not exist." ) continue diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 55e3d57ba..78a251c42 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -9,6 +9,7 @@ from typing import Any from peewee import DoesNotExist +from frigate.comms.config_updater import ConfigSubscriber from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum from frigate.comms.embeddings_updater import ( EmbeddingsRequestEnum, @@ -61,8 +62,8 @@ from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum from frigate.genai import get_genai_client from frigate.models import Event, Recordings, ReviewSegment, Trigger from frigate.util.builtin import serialize +from frigate.util.file import get_event_thumbnail_bytes from frigate.util.image import SharedMemoryFrameManager -from frigate.util.path import get_event_thumbnail_bytes from .embeddings import Embeddings @@ -95,6 +96,9 @@ class EmbeddingMaintainer(threading.Thread): CameraConfigUpdateEnum.semantic_search, ], ) + self.classification_config_subscriber = ConfigSubscriber( + "config/classification/custom/" + ) # Configure Frigate DB db = SqliteVecQueueDatabase( @@ -154,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( @@ -189,6 +195,7 @@ class EmbeddingMaintainer(threading.Thread): self.config, model_config, self.event_metadata_publisher, + self.requestor, self.metrics, ) ) @@ -220,7 +227,9 @@ class EmbeddingMaintainer(threading.Thread): for c in self.config.cameras.values() ): self.post_processors.append( - AudioTranscriptionPostProcessor(self.config, self.requestor, metrics) + AudioTranscriptionPostProcessor( + self.config, self.requestor, self.embeddings, metrics + ) ) semantic_trigger_processor: SemanticTriggerProcessor | None = None @@ -229,6 +238,7 @@ class EmbeddingMaintainer(threading.Thread): db, self.config, self.requestor, + self.event_metadata_publisher, metrics, self.embeddings, ) @@ -255,6 +265,7 @@ class EmbeddingMaintainer(threading.Thread): """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() @@ -265,6 +276,7 @@ class EmbeddingMaintainer(threading.Thread): 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() @@ -275,6 +287,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""" @@ -327,7 +401,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: @@ -337,6 +418,9 @@ class EmbeddingMaintainer(threading.Thread): # 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 @@ -345,6 +429,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: @@ -353,7 +438,11 @@ 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) for processor in self.post_processors: diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index d5e6ca3fb..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__) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index c33856db4..d7724c648 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -150,10 +150,10 @@ PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}", "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}", - FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}", - FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} {3} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -hwaccel device {3} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile main {2}", FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", @@ -246,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", ) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 7bc1bbf75..dd42fc6dd 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -51,8 +51,7 @@ class GenAIClient: 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: + return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring: - {concern_list}""" else: return "" @@ -63,58 +62,68 @@ class GenAIClient: else: return "" - def get_verified_objects() -> str: - if review_data["recognized_objects"]: - return " - " + "\n - ".join(review_data["recognized_objects"]) + def get_objects_list() -> str: + if review_data["unified_objects"]: + return "\n- " + "\n- ".join(review_data["unified_objects"]) else: - return " None" + return "\n- (No objects detected)" context_prompt = f""" -Please analyze the sequence of images ({len(thumbnails)} total) taken in chronological order from the perspective of the {review_data["camera"].replace("_", " ")} security camera. +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 -**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 whether the observable evidence suggests normal activity for this property or genuine security concerns. -3. Assigns a potential_threat_level based on the definitions below, applying them consistently. +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. -**IMPORTANT: Start by checking if the activity matches the normal patterns above. If it does, assign Level 0. Only consider higher threat levels if the activity clearly deviates from normal patterns or shows genuine security concerns.** +**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 "Detected objects" below.** Do not infer or mention additional people, vehicles, or objects not present in the detected objects list, even if visual patterns suggest them. If only a car is detected, do not describe a person interacting with it unless "person" is also in the detected objects list. +- **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. -- Identify patterns that suggest genuine security concerns: testing doors/windows on vehicles or buildings, accessing unauthorized areas, attempting to conceal actions, extended loitering without apparent purpose, taking items, behavior that clearly doesn't align with the zone context and detected objects. -- **Weigh all evidence holistically**: Start by checking if the activity matches the normal patterns above. If it does, assign Level 0. Only consider Level 1 if the activity clearly deviates from normal patterns or shows genuine security concerns that warrant attention. +- **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, one-sentence title that captures the main activity. Include any verified recognized objects (from the "Verified recognized objects" list below) and key detected objects. Examples: "Joe walking dog in backyard", "Unknown person testing car doors at night". -- `scene` (string): A narrative description of what happens across the sequence from start to finish. **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. +- `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. - `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 below. Your threat level must be consistent with your scene description and the guidance above. +- `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()} -Threat-level definitions: -- 0 — **Normal activity (DEFAULT)**: What you observe matches the normal activity patterns above or is consistent with expected activity for this property type. The observable evidence—considering zone context, detected objects, and timing together—supports a benign explanation. **Use this level for routine activities even if minor ambiguous elements exist.** -- 1 — **Potentially suspicious**: Observable behavior raises genuine security concerns that warrant human review. The evidence doesn't support a routine explanation and clearly deviates from the normal patterns above. Examples: testing doors/windows on vehicles or structures, accessing areas that don't align with the activity, taking items that likely don't belong to them, behavior clearly inconsistent with the zone and context, or activity that lacks any visible legitimate indicators. **Only use this level when the activity clearly doesn't match normal patterns.** -- 2 — **Immediate threat**: Clear evidence of forced entry, break-in, vandalism, aggression, weapons, theft in progress, or active property damage. +## Sequence Details -Sequence details: - Frame 1 = earliest, Frame {len(thumbnails)} = latest - Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds -- Detected objects: {", ".join(review_data["objects"])} -- Verified recognized objects (use these names when describing these objects): -{get_verified_objects()} -- Zones involved: {", ".join(z.replace("_", " ").title() for z in review_data["zones"]) or "None"} +- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"} -**IMPORTANT:** +## 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 "Detected objects" list above. Do not hallucinate additional objects. +- 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( @@ -149,7 +158,8 @@ Sequence details: try: metadata = ReviewMetadata.model_validate_json(clean_json) - if review_data["recognized_objects"]: + # 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"] diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index 30247e31c..9f9c8a750 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -1,7 +1,7 @@ """Ollama Provider for Frigate AI.""" import logging -from typing import Optional +from typing import Any, Optional from httpx import TimeoutException from ollama import Client as ApiClient @@ -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 @@ -48,10 +62,13 @@ class OllamaClient(GenAIClient): self.genai_config.model, prompt, images=images if images else None, - **self.genai_config.provider_options, + **self.provider_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, ConnectionError) as e: logger.warning("Ollama returned an error: %s", str(e)) return None diff --git a/frigate/genai/openai.py b/frigate/genai/openai.py index 046a18aa9..631cb3480 100644 --- a/frigate/genai/openai.py +++ b/frigate/genai/openai.py @@ -18,6 +18,7 @@ class OpenAIClient(GenAIClient): """Generative AI client for Frigate using OpenAI.""" provider: OpenAI + context_size: Optional[int] = None def _init_provider(self): """Initialize the client.""" @@ -69,5 +70,33 @@ class OpenAIClient(GenAIClient): def get_context_size(self) -> int: """Get the context window size for OpenAI.""" - # OpenAI GPT-4 Vision models have 128K token context window - return 128000 + if self.context_size is not None: + 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/object_detection/base.py b/frigate/object_detection/base.py index 9f4965111..bb5f83fab 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -9,6 +9,7 @@ from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent import numpy as np +import zmq from frigate.comms.object_detector_signaler import ( ObjectDetectorPublisher, @@ -377,6 +378,15 @@ 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.detection_queue.put(self.name) diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 0939b5ce4..eb23c2573 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -9,6 +9,7 @@ import os import queue import subprocess as sp import threading +import time import traceback from typing import Any, Optional @@ -791,6 +792,10 @@ class Birdseye: 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( @@ -848,6 +853,15 @@ class Birdseye: 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.converter.join() diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 4ee6f48b1..e15690e58 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__) diff --git a/frigate/record/export.py b/frigate/record/export.py index 1d56baf15..d4b49bb4b 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -28,7 +28,7 @@ from frigate.ffmpeg_presets import ( 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__) diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 40d7b64cf..917c0c5ac 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -407,6 +407,19 @@ class ReviewSegmentMaintainer(threading.Thread): segment.last_detection_time = frame_time for object in activity.get_all_objects(): + # Alert-level objects should always be added (they extend/upgrade the segment) + # Detection-level objects should only be added if: + # - The segment is a detection segment (matching severity), OR + # - The segment is an alert AND the object started before the alert ended + # (objects starting after will be in the new detection segment) + is_alert_object = object in activity.categorized_objects["alerts"] + + if not is_alert_object and segment.severity == SeverityEnum.alert: + # This is a detection-level object + # Only add if it started during the alert's active period + if object["start_time"] > segment.last_alert_time: + continue + if not object["sub_label"]: segment.detections[object["id"]] = object["label"] elif object["sub_label"][0] in self.config.model.all_attributes: diff --git a/frigate/stats/util.py b/frigate/stats/util.py index 1f92ffbe0..17b45d1d4 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -25,6 +25,7 @@ from frigate.util.services import ( 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, @@ -247,6 +248,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 @@ -357,7 +362,7 @@ def stats_snapshot( stats["embeddings"]["review_description_speed"] = round( embeddings_metrics.review_desc_speed.value * 1000, 2 ) - stats["embeddings"]["review_descriptions"] = round( + stats["embeddings"]["review_description_events_per_second"] = round( embeddings_metrics.review_desc_dps.value, 2 ) @@ -365,7 +370,7 @@ def stats_snapshot( stats["embeddings"]["object_description_speed"] = round( embeddings_metrics.object_desc_speed.value * 1000, 2 ) - stats["embeddings"]["object_descriptions"] = round( + stats["embeddings"]["object_description_events_per_second"] = round( embeddings_metrics.object_desc_dps.value, 2 ) @@ -373,7 +378,7 @@ def stats_snapshot( stats["embeddings"][f"{key}_classification_speed"] = round( embeddings_metrics.classification_speeds[key].value * 1000, 2 ) - stats["embeddings"][f"{key}_classification"] = round( + stats["embeddings"][f"{key}_classification_events_per_second"] = round( embeddings_metrics.classification_cps[key].value, 2 ) diff --git a/frigate/storage.py b/frigate/storage.py index 611412e1e..ee11cf7a9 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -113,6 +113,7 @@ class StorageMaintainer(threading.Thread): recordings: Recordings = ( Recordings.select( Recordings.id, + Recordings.camera, Recordings.start_time, Recordings.end_time, Recordings.segment_size, @@ -137,7 +138,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: @@ -172,7 +173,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 @@ -186,6 +187,9 @@ class StorageMaintainer(threading.Thread): recordings = ( Recordings.select( Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, Recordings.path, Recordings.segment_size, ) @@ -201,7 +205,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 @@ -211,7 +215,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/test_http_media.py b/frigate/test/http_api/test_http_media.py new file mode 100644 index 000000000..970a331e7 --- /dev/null +++ b/frigate/test/http_api/test_http_media.py @@ -0,0 +1,379 @@ +"""Unit tests for recordings/media API endpoints.""" + +from datetime import datetime, timezone +from typing import Any + +import pytz +from fastapi.testclient import TestClient + +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 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 auth to bypass camera access for tests + async def mock_get_current_user(request: Any): + return {"username": "test_user", "role": "admin"} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [ + "front_door", + "back_door", + ] + + 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 TestClient(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 TestClient(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 TestClient(self.app) as client: + # 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 + + 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 TestClient(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 TestClient(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 TestClient(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 TestClient(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/timeline.py b/frigate/timeline.py index 4c3d0d457..8e6aedc67 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -142,6 +142,14 @@ class TimelineProcessor(threading.Thread): timeline_entry[Timeline.data]["attribute"] = list( event_data["attributes"].keys() )[0] + + if len(event_data["current_attributes"]) > 0: + timeline_entry[Timeline.data]["attribute_box"] = to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["current_attributes"][0]["box"], + ) + save = True elif event_type == EventStateEnum.end: timeline_entry[Timeline.class_type] = "gone" diff --git a/frigate/types.py b/frigate/types.py index a9e27ba90..6c5135616 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -23,9 +23,11 @@ class ModelStatusTypesEnum(str, Enum): 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/builtin.py b/frigate/util/builtin.py index 5ab29a6ea..b1a76214b 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -15,12 +15,9 @@ from collections.abc import Mapping from multiprocessing.sharedctypes import Synchronized from pathlib import Path from typing import Any, Dict, Optional, Tuple, Union -from zoneinfo import ZoneInfoNotFoundError 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]: @@ -298,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(): diff --git a/frigate/util/classification.py b/frigate/util/classification.py index e4133ded4..a74094c32 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -1,13 +1,18 @@ """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, @@ -15,16 +20,105 @@ from frigate.const import ( UPDATE_MODEL_STATE, ) from frigate.log import redirect_output_to_logger +from frigate.models import Event, Recordings, ReviewSegment from frigate.types import ModelStatusTypesEnum +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 + """ + 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 + """ + 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 + """ + 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: super().__init__( @@ -36,7 +130,8 @@ class ClassificationTrainingProcess(FrigateProcess): def run(self) -> None: self.pre_run_setup() - self.__train_classification_model() + success = self.__train_classification_model() + exit(0 if success else 1) def __generate_representative_dataset_factory(self, dataset_dir: str): def generate_representative_dataset(): @@ -59,87 +154,119 @@ class ClassificationTrainingProcess(FrigateProcess): @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 - # 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) - logger.info(f"Kicking off classification training for {self.model_name}.") - dataset_dir = os.path.join(CLIPS_DIR, self.model_name, "dataset") - model_dir = os.path.join(MODEL_CACHE_DIR, self.model_name) - num_classes = len( - [ - d - for d in os.listdir(dataset_dir) - if os.path.isdir(os.path.join(dataset_dir, d)) - ] - ) + num_classes = len( + [ + d + for d in os.listdir(dataset_dir) + if os.path.isdir(os.path.join(dataset_dir, d)) + ] + ) - # Start with imagenet base model with 35% of channels in each layer - base_model = MobileNetV2( - input_shape=(224, 224, 3), - include_top=False, - weights="imagenet", - alpha=0.35, - ) - base_model.trainable = False # Freeze pre-trained layers + if num_classes < 2: + logger.error( + f"Training failed for {self.model_name}: Need at least 2 classes, found {num_classes}" + ) + return False - model = models.Sequential( - [ - base_model, - layers.GlobalAveragePooling2D(), - layers.Dense(128, activation="relu"), - layers.Dropout(0.3), - layers.Dense(num_classes, activation="softmax"), - ] - ) + # Start with imagenet base model with 35% of channels in each layer + base_model = MobileNetV2( + input_shape=(224, 224, 3), + include_top=False, + weights="imagenet", + alpha=0.35, + ) + base_model.trainable = False # Freeze pre-trained layers - model.compile( - optimizer=optimizers.Adam(learning_rate=LEARNING_RATE), - loss="categorical_crossentropy", - metrics=["accuracy"], - ) + model = models.Sequential( + [ + base_model, + layers.GlobalAveragePooling2D(), + layers.Dense(128, activation="relu"), + layers.Dropout(0.3), + layers.Dense(num_classes, activation="softmax"), + ] + ) - # 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", - ) + model.compile( + optimizer=optimizers.Adam(learning_rate=LEARNING_RATE), + loss="categorical_crossentropy", + metrics=["accuracy"], + ) - # 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") + # 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", + ) - # train the model - model.fit(train_gen, epochs=EPOCHS, verbose=0) + total_images = train_gen.samples + logger.debug( + f"Training {self.model_name}: {total_images} images across {num_classes} classes" + ) - # convert model to tflite - 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 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") - # write model - with open(os.path.join(model_dir, "model.tflite"), "wb") as f: - f.write(tflite_model) + # 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 + 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 -@staticmethod def kickoff_model_training( embeddingRequestor: EmbeddingsRequestor, model_name: str ) -> None: @@ -159,16 +286,551 @@ def kickoff_model_training( training_process.start() training_process.join() - # 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, - }, - ) + # 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 20 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) + """ + 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: 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 + + # 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. + + 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 + """ + keyframe_paths = [] + + for idx, ts_info in enumerate(timestamps): + camera = ts_info["camera"] + timestamp = ts_info["timestamp"] + + if camera not in camera_crops: + logger.warning(f"No crop coordinates for camera {camera}") + continue + + 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: + continue + + 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 image_data: + nparr = np.frombuffer(image_data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is not 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 and y2_clipped > y1_clipped: + 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) + keyframe_paths.append(output_path) + + except Exception as e: + logger.debug( + f"Failed to extract frame from {recording.path} at {relative_time}s: {e}" + ) + continue + + return keyframe_paths + + +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") + cameras: List of camera names to collect examples from + """ + 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 56f5662fc..5a14b1fa6 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -384,10 +384,10 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] new_object_config["genai"] = {} for key in global_genai.keys(): - if key not in ["enabled", "model", "provider", "base_url", "api_key"]: - new_object_config["genai"][key] = global_genai[key] - else: + 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] config["genai"] = new_genai_config 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/model.py b/frigate/util/model.py index 308a16689..338303e2d 100644 --- a/frigate/util/model.py +++ b/frigate/util/model.py @@ -369,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/path.py b/frigate/util/path.py deleted file mode 100644 index 6a62bd44c..000000000 --- a/frigate/util/path.py +++ /dev/null @@ -1,62 +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.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 diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py index 48fc0139e..8b2fd0050 100644 --- a/frigate/util/rknn_converter.py +++ b/frigate/util/rknn_converter.py @@ -1,6 +1,5 @@ """RKNN model conversion utility for Frigate.""" -import fcntl import logging import os import subprocess @@ -9,6 +8,8 @@ import time from pathlib import Path from typing import Optional +from frigate.util.file import FileLock + logger = logging.getLogger(__name__) MODEL_TYPE_CONFIGS = { @@ -129,8 +130,13 @@ def get_soc_type() -> Optional[str]: """Get the SoC type from device tree.""" try: with open("/proc/device-tree/compatible") as file: - soc = file.read().split(",")[-1].strip("\x00") - return soc + 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 @@ -245,112 +251,6 @@ def convert_onnx_to_rknn( logger.warning(f"Failed to remove temporary ONNX file: {e}") -def cleanup_stale_lock(lock_file_path: Path) -> bool: - """ - Clean up a stale lock file if it exists and is old. - - Args: - lock_file_path: Path to the lock file - - Returns: - True if lock was cleaned up, False otherwise - """ - try: - if lock_file_path.exists(): - # Check if lock file is older than 10 minutes (stale) - lock_age = time.time() - lock_file_path.stat().st_mtime - if lock_age > 600: # 10 minutes - logger.warning( - f"Removing stale lock file: {lock_file_path} (age: {lock_age:.1f}s)" - ) - lock_file_path.unlink() - return True - except Exception as e: - logger.error(f"Error cleaning up stale lock: {e}") - - return False - - -def acquire_conversion_lock(lock_file_path: Path, timeout: int = 300) -> bool: - """ - Acquire a file-based lock for model conversion. - - Args: - lock_file_path: Path to the lock file - timeout: Maximum time to wait for lock in seconds - - Returns: - True if lock acquired, False if timeout or error - """ - try: - lock_file_path.parent.mkdir(parents=True, exist_ok=True) - cleanup_stale_lock(lock_file_path) - lock_fd = os.open(lock_file_path, os.O_CREAT | os.O_RDWR) - - # Try to acquire exclusive lock - start_time = time.time() - while time.time() - start_time < timeout: - try: - fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - # Lock acquired successfully - logger.debug(f"Acquired conversion lock: {lock_file_path}") - return True - except (OSError, IOError): - # Lock is held by another process, wait and retry - if time.time() - start_time >= timeout: - logger.warning( - f"Timeout waiting for conversion lock: {lock_file_path}" - ) - os.close(lock_fd) - return False - - logger.debug("Waiting for conversion lock to be released...") - time.sleep(1) - - os.close(lock_fd) - return False - - except Exception as e: - logger.error(f"Error acquiring conversion lock: {e}") - return False - - -def release_conversion_lock(lock_file_path: Path) -> None: - """ - Release the conversion lock. - - Args: - lock_file_path: Path to the lock file - """ - try: - if lock_file_path.exists(): - lock_file_path.unlink() - logger.debug(f"Released conversion lock: {lock_file_path}") - except Exception as e: - logger.error(f"Error releasing conversion lock: {e}") - - -def is_lock_stale(lock_file_path: Path, max_age: int = 600) -> bool: - """ - Check if a lock file is stale (older than max_age seconds). - - Args: - lock_file_path: Path to the lock file - max_age: Maximum age in seconds before considering lock stale - - Returns: - True if lock is stale, False otherwise - """ - try: - if lock_file_path.exists(): - lock_age = time.time() - lock_file_path.stat().st_mtime - return lock_age > max_age - except Exception: - pass - - return False - - def wait_for_conversion_completion( model_type: str, rknn_path: Path, lock_file_path: Path, timeout: int = 300 ) -> bool: @@ -358,6 +258,7 @@ def wait_for_conversion_completion( 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 @@ -366,6 +267,8 @@ def wait_for_conversion_completion( 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(): @@ -385,11 +288,14 @@ def wait_for_conversion_completion( return False # Check if lock is stale - if is_lock_stale(lock_file_path): + if lock.is_stale(): logger.warning("Lock file is stale, attempting to clean up and retry...") - cleanup_stale_lock(lock_file_path) + lock._cleanup_stale_lock() # Try to acquire lock again - if acquire_conversion_lock(lock_file_path, timeout=60): + 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(): @@ -415,7 +321,7 @@ def wait_for_conversion_completion( return False finally: - release_conversion_lock(lock_file_path) + retry_lock.release() logger.debug("Waiting for RKNN model to appear...") time.sleep(1) @@ -452,8 +358,9 @@ def auto_convert_model( 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 acquire_conversion_lock(lock_file_path): + if lock.acquire(): try: if rknn_path.exists(): logger.info( @@ -476,7 +383,7 @@ def auto_convert_model( return None finally: - release_conversion_lock(lock_file_path) + lock.release() else: logger.info( f"Another process is converting {model_path}, waiting for completion..." diff --git a/frigate/util/services.py b/frigate/util/services.py index ed21e7b00..c51fe923a 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -9,6 +9,7 @@ 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 @@ -388,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: @@ -543,7 +577,7 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro if detailed and format_entries: ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"]) - ffprobe_cmd.extend(["-loglevel", "quiet", clean_path]) + ffprobe_cmd.extend(["-loglevel", "error", clean_path]) return sp.run(ffprobe_cmd, capture_output=True) 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 2b88b24ff..6be4f52a4 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -16,7 +16,7 @@ from frigate.comms.recordings_updater import ( RecordingsDataSubscriber, RecordingsDataTypeEnum, ) -from frigate.config import CameraConfig, DetectConfig, ModelConfig +from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig from frigate.config.camera.camera import CameraTypeEnum from frigate.config.camera.updater import ( CameraConfigUpdateEnum, @@ -34,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, @@ -53,6 +53,7 @@ from frigate.util.object import ( reduce_detections, ) from frigate.util.process import FrigateProcess +from frigate.util.time import get_tomorrow_at_time logger = logging.getLogger(__name__) @@ -195,7 +196,9 @@ class CameraWatchdog(threading.Thread): self.sleeptime = self.config.ffmpeg.retry_interval self.config_subscriber = CameraConfigUpdateSubscriber( - None, {config.name: config}, [CameraConfigUpdateEnum.enabled] + None, + {config.name: config}, + [CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record], ) self.requestor = InterProcessRequestor() self.was_enabled = self.config.enabled @@ -536,6 +539,7 @@ class CameraCapture(FrigateProcess): shm_frame_count: int, camera_metrics: CameraMetrics, stop_event: MpEvent, + log_config: LoggerConfig | None = None, ) -> None: super().__init__( stop_event, @@ -546,9 +550,10 @@ class CameraCapture(FrigateProcess): self.config = config self.shm_frame_count = shm_frame_count self.camera_metrics = camera_metrics + self.log_config = log_config def run(self) -> None: - self.pre_run_setup() + self.pre_run_setup(self.log_config) camera_watchdog = CameraWatchdog( self.config, self.shm_frame_count, @@ -574,6 +579,7 @@ class CameraTracker(FrigateProcess): ptz_metrics: PTZMetrics, region_grid: list[list[dict[str, Any]]], stop_event: MpEvent, + log_config: LoggerConfig | None = None, ) -> None: super().__init__( stop_event, @@ -589,9 +595,10 @@ class CameraTracker(FrigateProcess): self.camera_metrics = camera_metrics self.ptz_metrics = ptz_metrics self.region_grid = region_grid + self.log_config = log_config def run(self) -> None: - self.pre_run_setup() + self.pre_run_setup(self.log_config) frame_queue = self.camera_metrics.frame_queue frame_shape = self.config.frame_shape 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/images/branding/LICENSE b/web/images/branding/LICENSE new file mode 100644 index 000000000..42913f9ca --- /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 LLC 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 LLC. Frigate LLC 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 LLC. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate LLC. + 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) 2025 Frigate LLC. 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 fe1bad521..371defaaa 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "@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.15", "@radix-ui/react-dropdown-menu": "^2.1.6", @@ -1380,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", diff --git a/web/package.json b/web/package.json index c00bd77dd..27256bd81 100644 --- a/web/package.json +++ b/web/package.json @@ -20,6 +20,7 @@ "@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.15", "@radix-ui/react-dropdown-menu": "^2.1.6", 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/views/classificationModel.json b/web/public/locales/ar/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/ar/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/bg/views/classificationModel.json b/web/public/locales/bg/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/bg/views/classificationModel.json @@ -0,0 +1 @@ +{} 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 ba8a11bb5..fa5ce3b62 100644 --- a/web/public/locales/ca/common.json +++ b/web/public/locales/ca/common.json @@ -207,10 +207,21 @@ "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" }, "button": { "apply": "Aplicar", @@ -270,5 +281,17 @@ "desc": "Pàgina no trobada" }, "selectItem": "Selecciona {{item}}", - "readTheDocumentation": "Llegir la documentació" + "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 6cbe2877c..1ca91ee7a 100644 --- a/web/public/locales/ca/components/auth.json +++ b/web/public/locales/ca/components/auth.json @@ -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/dialog.json b/web/public/locales/ca/components/dialog.json index 20988372a..0fa89afbe 100644 --- a/web/public/locales/ca/components/dialog.json +++ b/web/public/locales/ca/components/dialog.json @@ -53,7 +53,7 @@ "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", @@ -98,7 +98,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ó", @@ -116,6 +117,7 @@ "search": { "placeholder": "Cerca per etiqueta o subetiqueta..." }, - "noImages": "No s'han trobat miniatures per a aquesta càmera" + "noImages": "No s'han trobat miniatures per a aquesta càmera", + "unknownLabel": "Imatge activadora desada" } } diff --git a/web/public/locales/ca/views/classificationModel.json b/web/public/locales/ca/views/classificationModel.json new file mode 100644 index 000000000..b64214a89 --- /dev/null +++ b/web/public/locales/ca/views/classificationModel.json @@ -0,0 +1,164 @@ +{ + "documentTitle": "Models de classificació", + "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 el model {{count}}", + "deletedModel_many": "S'han suprimit correctament {{count}} models", + "deletedModel_other": "", + "updatedModel": "S'ha actualitzat correctament la configuració del model" + }, + "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": "No s'ha pogut iniciar l'entrenament del model: {{errorMessage}}", + "deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}", + "updateModelFailed": "No s'ha pogut actualitzar el 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." + }, + "deleteDatasetImages": { + "title": "Suprimeix les imatges del conjunt de dades", + "desc": "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": "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 a 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ó" + }, + "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" + } + }, + "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": "Esteu segur que voleu suprimir {{count}} model(s)? 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." + }, + "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." + } +} diff --git a/web/public/locales/ca/views/events.json b/web/public/locales/ca/views/events.json index 99d56c1d9..2bb9bc0e1 100644 --- a/web/public/locales/ca/views/events.json +++ b/web/public/locales/ca/views/events.json @@ -36,5 +36,24 @@ "selected_one": "{{count}} seleccionats", "selected_other": "{{count}} seleccionats", "suspiciousActivity": "Activitat sospitosa", - "threateningActivity": "Activitat amenaçadora" + "threateningActivity": "Activitat amenaçadora", + "detail": { + "noDataFound": "No hi ha dades detallades a revisar", + "trackedObject_one": "objecte", + "aria": "Canvia la vista de detall", + "trackedObject_other": "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" } diff --git a/web/public/locales/ca/views/explore.json b/web/public/locales/ca/views/explore.json index 3b79a696f..d45f92665 100644 --- a/web/public/locales/ca/views/explore.json +++ b/web/public/locales/ca/views/explore.json @@ -84,7 +84,8 @@ "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" }, "details": { "timestamp": "Marca temporal", @@ -206,13 +207,23 @@ "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" } }, "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}}", @@ -224,5 +235,53 @@ }, "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" + } + }, + "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": "CONSELL: Imagineu-vos que hi ha un clip d'esdeveniment amb una persona caminant d'esquerra a dreta. Si el quadre delimitador de la cronologia de l'esdeveniment està constantment a l'esquerra de la persona, aleshores s'hauria de disminuir el valor. De la mateixa manera, si una persona camina d'esquerra a dreta i el quadre delimitador està constantment per davant de la persona, aleshores s'hauria d'augmentar el valor.", + "toast": { + "success": "L'Òfset d'anotació per a {{camera}} s'ha desat al fitxer de configuració. Reinicieu Frigate per aplicar els canvis." + } + } + }, + "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..f99629bdb 100644 --- a/web/public/locales/ca/views/faceLibrary.json +++ b/web/public/locales/ca/views/faceLibrary.json @@ -12,13 +12,13 @@ "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" }, "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 +54,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", diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json index f4d20fc70..f98b33d62 100644 --- a/web/public/locales/ca/views/live.json +++ b/web/public/locales/ca/views/live.json @@ -86,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." @@ -130,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ó", @@ -167,5 +170,16 @@ "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" } } diff --git a/web/public/locales/ca/views/search.json b/web/public/locales/ca/views/search.json index 3f5940348..dec453728 100644 --- a/web/public/locales/ca/views/search.json +++ b/web/public/locales/ca/views/search.json @@ -55,12 +55,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 900c6b3c0..36a041510 100644 --- a/web/public/locales/ca/views/settings.json +++ b/web/public/locales/ca/views/settings.json @@ -9,7 +9,9 @@ "masksAndZones": "Editor de màscares i zones - Frigate", "general": "Paràmetres Generals - Frigate", "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", @@ -21,7 +23,10 @@ "debug": "Depuració", "frigateplus": "Frigate+", "enrichments": "Enriquiments", - "triggers": "Disparadors" + "triggers": "Disparadors", + "cameraManagement": "Gestió", + "cameraReview": "Revisió", + "roles": "Rols" }, "dialog": { "unsavedChanges": { @@ -44,6 +49,10 @@ "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." } }, "storedLayouts": { @@ -109,7 +118,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": { @@ -158,7 +168,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." }, "label": "Zones", "desc": { @@ -689,7 +699,9 @@ }, "actions": { "alert": "Marcar com Alerta", - "notification": "Enviar Notificació" + "notification": "Enviar Notificació", + "sub_label": "Afegeix una subetiqueta", + "attribute": "Afegeix un atribut" }, "dialog": { "createTrigger": { @@ -707,25 +719,28 @@ "form": { "name": { "title": "Nom", - "placeholder": "Entrar el nom del disparador", + "placeholder": "Anomena aquest activador", "error": { - "minLength": "El nom ha de tenir almenys 2 caràcters de llargada.", - "invalidCharacters": "El nom només pot contenir lletres, números, guions i guinons baixos.", + "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" + "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 imatge", + "imagePlaceholder": "Selecciona una miniatura", "textPlaceholder": "Entra el contingut de text", - "imageDesc": "Selecciona una imatge per disparar aquesta acció quan una imatge similar sigui detectada.", + "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." @@ -736,14 +751,20 @@ "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 tots els disparadors. Tria una acció adicional per realitzar quan aquest disparador dispari.", + "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." } } }, @@ -761,10 +782,31 @@ }, "documentTitle": "Disparadors", "management": { - "title": "Gestió de disparadors", + "title": "Activadors", "desc": "Gestionar els disparadors de {{camera}}. Usa les 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" + "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": { @@ -816,7 +858,9 @@ "createRole": "Rol {{role}} creat exitosament", "updateCameras": "Càmeres actualitzades per al rol {{role}}", "deleteRole": "Rol {{role}} eliminat exitosament", - "userRolesUpdated": "{{count}} usuari(s) asignats a aquest rol s'han actualitzat a 'visor', i tenen accés a totes les càmeres." + "userRolesUpdated_one": "{{count}} usuari(s) asignats a aquest rol s'han actualitzat a 'visor', i tenen accés a totes les càmeres.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Error al crear el rol: {{errorMessage}}", @@ -825,5 +869,231 @@ "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" + }, + "step1": { + "cameraBrand": "Marca de la càmera", + "description": "Introduïu els detalls de la càmera i proveu la connexió.", + "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..." + } + }, + "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": "Configura els rols de flux i afegeix fluxos addicionals per a la càmera.", + "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 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": "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" + }, + "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": "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 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." + } + }, + "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/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/views/classificationModel.json b/web/public/locales/cs/views/classificationModel.json new file mode 100644 index 000000000..a8d060290 --- /dev/null +++ b/web/public/locales/cs/views/classificationModel.json @@ -0,0 +1,7 @@ +{ + "documentTitle": "Klasifikační modely", + "button": { + "deleteClassificationAttempts": "Odstranit Klasifikační obrazy", + "renameCategory": "Přejmenovat třídu" + } +} diff --git a/web/public/locales/cs/views/faceLibrary.json b/web/public/locales/cs/views/faceLibrary.json index 8db564c37..58751f810 100644 --- a/web/public/locales/cs/views/faceLibrary.json +++ b/web/public/locales/cs/views/faceLibrary.json @@ -41,7 +41,7 @@ "aria": "Vybrat trénink" }, "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." }, diff --git a/web/public/locales/cs/views/settings.json b/web/public/locales/cs/views/settings.json index f4179763c..c0ff72f5f 100644 --- a/web/public/locales/cs/views/settings.json +++ b/web/public/locales/cs/views/settings.json @@ -10,7 +10,9 @@ "object": "Ladění - Frigate", "general": "Obecné nastavení - 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": { @@ -845,7 +847,9 @@ "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": "{{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_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}}", diff --git a/web/public/locales/da/views/classificationModel.json b/web/public/locales/da/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/da/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json index 2d20c7f0c..98c3f4d7a 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -232,6 +232,14 @@ "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": { @@ -273,5 +281,8 @@ "desc": "Du hast keine Berechtigung diese Seite anzuzeigen.", "documentTitle": "Zugang verweigert - Frigate", "title": "Zugang verweigert" + }, + "information": { + "pixels": "{{area}}px" } } diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json index 578f02773..4ef555e76 100644 --- a/web/public/locales/de/components/dialog.json +++ b/web/public/locales/de/components/dialog.json @@ -117,7 +117,8 @@ "button": { "export": "Exportieren", "markAsReviewed": "Als geprüft markieren", - "deleteNow": "Jetzt löschen" + "deleteNow": "Jetzt löschen", + "markAsUnreviewed": "Als ungeprüft markieren" } }, "imagePicker": { 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..0967ef424 --- /dev/null +++ b/web/public/locales/de/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json index cda458b65..b9df73594 100644 --- a/web/public/locales/de/views/faceLibrary.json +++ b/web/public/locales/de/views/faceLibrary.json @@ -29,7 +29,7 @@ "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." diff --git a/web/public/locales/de/views/live.json b/web/public/locales/de/views/live.json index fea1cabd8..7ab230b20 100644 --- a/web/public/locales/de/views/live.json +++ b/web/public/locales/de/views/live.json @@ -30,16 +30,16 @@ }, "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": { @@ -62,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", @@ -74,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." @@ -88,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." @@ -118,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": { @@ -167,5 +170,16 @@ "transcription": { "enable": "Live Audio Transkription einschalten", "disable": "Live Audio Transkription ausschalten" + }, + "noCameras": { + "title": "Keine Kameras eingerichtet", + "description": "Beginne indem du eine Kamera anschließt.", + "buttonText": "Kamera hinzufügen" + }, + "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/settings.json b/web/public/locales/de/views/settings.json index 80902106f..be4ec3259 100644 --- a/web/public/locales/de/views/settings.json +++ b/web/public/locales/de/views/settings.json @@ -10,7 +10,9 @@ "classification": "Klassifizierungseinstellungen – Frigate", "motionTuner": "Bewegungserkennungs-Optimierer – Frigate", "notifications": "Benachrichtigungs-Einstellungen", - "enrichments": "Erweiterte Statistiken - Frigate" + "enrichments": "Erweiterte Statistiken - Frigate", + "cameraManagement": "Kameras verwalten - Frigate", + "cameraReview": "Kamera Einstellungen prüfen - Frigate" }, "menu": { "ui": "Benutzeroberfläche", @@ -23,7 +25,10 @@ "users": "Benutzer", "notifications": "Benachrichtigungen", "enrichments": "Erkennungsfunktionen", - "triggers": "Auslöser" + "triggers": "Auslöser", + "roles": "Rollen", + "cameraManagement": "Verwaltung", + "cameraReview": "Überprüfung" }, "dialog": { "unsavedChanges": { @@ -69,7 +74,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" } @@ -812,6 +817,11 @@ "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." } } }, @@ -826,6 +836,10 @@ "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." } }, "roles": { @@ -878,7 +892,8 @@ "createRole": "Rolle {{role}} erfolgreich erstellt", "updateCameras": "Kameras für Rolle {{role}} aktualisiert", "deleteRole": "Rolle {{role}} erfolgreich gelöscht", - "userRolesUpdated": "{{count}} Benutzer, denen diese Rolle zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras hat." + "userRolesUpdated_one": "{{count}} Benutzer, denen diese Rolle zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras hat.", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Fehler beim Erstellen der Rolle: {{errorMessage}}", @@ -887,5 +902,222 @@ "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" + }, + "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": "Gib deine Kameradaten ein und teste die Verbindung.", + "cameraName": "Kamera-Name", + "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": "Kamera-Hersteller", + "selectBrand": "Wähle die Kamera-Hersteller 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 Kamera-Hersteller mit Host/IP aus oder wählen Sie „Andere“ mit einer benutzerdefinierten URL", + "nameRequired": "Kamera-Name benötigt", + "nameLength": "Kamera-Name darf höchsten 64 Zeichen lang sein", + "invalidCharacters": "Kamera-Name enthält ungültige Zeichen", + "nameExists": "Kamera-Name 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." + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "step2": { + "description": "Konfigurieren Sie Stream-Rollen und fügen Sie zusätzliche Streams für Ihre Kamera hinzu.", + "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": "Stream erfolgreich getestet!", + "testFailed": "Stream-Test 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": "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." + } + }, + "step3": { + "description": "Endgültige Validierung und Analyse vor dem Speichern Ihrer neuen Kamera. Verbinde jeden Stream vor dem Speichern.", + "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." + } + } + } + }, + "cameraManagement": { + "title": "Kameras verwalten", + "addCamera": "Neue Kamera hinzufügen", + "editCamera": "Kamera bearbeiten:", + "selectCamera": "Wähle eine Kamera", + "backToSettings": "Zurück zu Kamera-Einstellungen", + "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": "Kamera-Name", + "nameRequired": "Kamera-Name benötigt", + "nameLength": "Kamera-Name 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": "Review", + "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 car 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/el/common.json b/web/public/locales/el/common.json index 04c1dc01b..5cc5277b7 100644 --- a/web/public/locales/el/common.json +++ b/web/public/locales/el/common.json @@ -1,10 +1,10 @@ { "time": { - "untilForTime": "Ως{{time}}", + "untilForTime": "Ως {{time}}", "untilForRestart": "Μέχρι να γίνει επανεκίννηση του Frigate.", "untilRestart": "Μέχρι να γίνει επανεκκίνηση", "justNow": "Μόλις τώρα", - "ago": "{{timeAgo}} Πριν", + "ago": "Πριν {{timeAgo}}", "today": "Σήμερα", "yesterday": "Εχθές", "last7": "Τελευταίες 7 ημέρες", @@ -31,7 +31,44 @@ "lastMonth": "Τελευταίος Μήνας", "5minutes": "5 λεπτά", "10minutes": "10 λεπτά", - "30minutes": "30 λεπτά" + "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" + } }, "menu": { "live": { @@ -40,5 +77,49 @@ "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": "Πληροφορίες" + }, + "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": "Επιστροφή" } } diff --git a/web/public/locales/el/views/classificationModel.json b/web/public/locales/el/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/el/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/el/views/live.json b/web/public/locales/el/views/live.json index 8542f02be..b2427114e 100644 --- a/web/public/locales/el/views/live.json +++ b/web/public/locales/el/views/live.json @@ -62,5 +62,8 @@ "audioDetect": { "enable": "Ενεργοποίηση Ανίχνευσης Ήχου", "disable": "Απενεργοποίηση Ανίχνευσης Ήχου" + }, + "noCameras": { + "buttonText": "Προσθήκη Κάμερας" } } diff --git a/web/public/locales/el/views/settings.json b/web/public/locales/el/views/settings.json index 43a5a4a52..909bc57e6 100644 --- a/web/public/locales/el/views/settings.json +++ b/web/public/locales/el/views/settings.json @@ -42,5 +42,15 @@ "cameraSetting": { "camera": "Κάμερα", "noCamera": "Δεν υπάρχει Κάμερα" + }, + "triggers": { + "dialog": { + "form": { + "friendly_name": { + "placeholder": "Ονομάτισε ή περιέγραψε αυτό το εύνασμα", + "description": "Ένα προαιρετικό φιλικό όνομα, ή ένα περιγραφικό κείμενο για αυτό το εύνασμα." + } + } + } } } diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 924dc0d0e..aa841c30b 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -72,7 +72,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": { @@ -93,7 +96,21 @@ } }, "label": { - "back": "Go back" + "back": "Go back", + "hide": "Hide {{item}}", + "show": "Show {{item}}", + "ID": "ID", + "none": "None", + "all": "All" + }, + "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", @@ -130,7 +147,8 @@ "unselect": "Unselect", "export": "Export", "deleteNow": "Delete Now", - "next": "Next" + "next": "Next", + "continue": "Continue" }, "menu": { "system": "System", @@ -223,6 +241,7 @@ "export": "Export", "uiPlayground": "UI Playground", "faceLibrary": "Face Library", + "classification": "Classification", "user": { "title": "User", "account": "Account", 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/dialog.json b/web/public/locales/en/components/dialog.json index d502d5cc8..a40e62db7 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -52,7 +52,7 @@ "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.", "error": { "failed": "Failed to start export: {{error}}", "endTimeMustAfterStartTime": "End time must be after start time", @@ -112,6 +112,7 @@ }, "imagePicker": { "selectImage": "Select a tracked object's thumbnail", + "unknownLabel": "Saved Trigger Image", "search": { "placeholder": "Search by label or sub label..." }, diff --git a/web/public/locales/en/config/face_recognition.json b/web/public/locales/en/config/face_recognition.json index ec6f8929b..705d75468 100644 --- a/web/public/locales/en/config/face_recognition.json +++ b/web/public/locales/en/config/face_recognition.json @@ -23,7 +23,7 @@ "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 train tab." + "label": "Number of face attempts to save in the recent recognitions tab." }, "blur_confidence_filter": { "label": "Apply blur quality filter to face confidence." diff --git a/web/public/locales/en/config/review.json b/web/public/locales/en/config/review.json index a44c2cfa9..dba83ee1c 100644 --- a/web/public/locales/en/config/review.json +++ b/web/public/locales/en/config/review.json @@ -71,4 +71,4 @@ } } } -} \ No newline at end of file +} diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index 47b2b13bf..f8aef1b8f 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -1,37 +1,74 @@ { + "documentTitle": "Classification Models", + "details": { + "scoreInfo": "Score represents the average classification confidence across all detections of this object." + }, "button": { "deleteClassificationAttempts": "Delete Classification Images", "renameCategory": "Rename Class", "deleteCategory": "Delete Class", "deleteImages": "Delete Images", - "trainModel": "Train Model" + "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." + "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": "Failed to start model training: {{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." + "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": "Are you sure you want to delete {{count}} images from {{dataset}}? This action cannot be undone and will require re-training the model." + "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": "Are you sure you want to delete {{count}} images? This action cannot be undone." + "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", @@ -41,13 +78,102 @@ "invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens." }, "train": { - "title": "Train", - "aria": "Select Train" + "title": "Recent Classifications", + "titleShort": "Recent", + "aria": "Select Recent Classifications" }, "categories": "Classes", "createCategory": { "new": "Create New Class" }, "categorizeImageAs": "Classify Image As:", - "categorizeImage": "Classify Image" + "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", + "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", + "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" + } + } } diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index 77c626adf..d3cf78658 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -13,11 +13,30 @@ }, "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" diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index f35cfdc1d..19559a43a 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -33,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", @@ -71,9 +72,9 @@ }, "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.", + "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." } @@ -102,7 +103,7 @@ "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.", - "audioTranscription": "Successfully requested audio transcription." + "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}}", @@ -168,9 +169,9 @@ "label": "Download snapshot", "aria": "Download snapshot" }, - "viewObjectLifecycle": { - "label": "View object lifecycle", - "aria": "Show the object lifecycle" + "viewTrackingDetails": { + "label": "View tracking details", + "aria": "Show the tracking details" }, "findSimilar": { "label": "Find similar", @@ -194,12 +195,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", @@ -208,6 +215,8 @@ "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.", 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 3a0804511..453abfc22 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -1,16 +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": { - "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": { @@ -19,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", @@ -33,12 +28,10 @@ } }, "train": { - "title": "Train", - "aria": "Select train", + "title": "Recent Recognitions", + "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." @@ -69,7 +62,6 @@ "maxSize": "Max size: {{size}}MB" }, "nofaces": "No faces available", - "pixels": "{{area}}px", "trainFaceAs": "Train Face as:", "trainFace": "Train Face", "toast": { @@ -83,7 +75,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 2534c3957..21f367ea9 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -175,8 +175,12 @@ "exitEdit": "Exit Editing" }, "noCameras": { - "title": "No Cameras Set Up", - "description": "Get started by connecting a camera.", - "buttonText": "Add Camera" + "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." + } } } diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 16290ea80..b08aa10c0 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -8,7 +8,7 @@ "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" }, @@ -37,7 +37,7 @@ "noCamera": "No Camera" }, "general": { - "title": "General Settings", + "title": "UI Settings", "liveDashboard": { "title": "Live Dashboard", "automaticLiveView": { @@ -47,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": { @@ -150,6 +158,7 @@ "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" }, @@ -168,7 +177,7 @@ "testFailed": "Stream test failed: {{error}}" }, "step1": { - "description": "Enter your camera details and test the connection.", + "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", @@ -184,28 +193,65 @@ "brandInformation": "Brand information", "brandUrlFormat": "For cameras with the RTSP URL format as: {{exampleUrl}}", "customUrlPlaceholder": "rtsp://username:password@host:port/path", - "testConnection": "Test Connection", - "testSuccess": "Connection test successful!", - "testFailed": "Connection test failed. Please check your input and try again.", - "streamDetails": "Stream Details", - "warnings": { - "noSnapshot": "Unable to fetch a snapshot from the configured stream." - }, + "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", - "brands": { - "reolink-rtsp": "Reolink RTSP is not recommended. It is recommended to enable http in the camera settings and restart the camera wizard." - } - }, - "docs": { - "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + "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", @@ -213,6 +259,9 @@ "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", @@ -244,7 +293,7 @@ "description": "Use go2rtc restreaming to reduce connections to your camera." } }, - "step3": { + "step4": { "description": "Final validation and analysis before saving your new camera. Connect each stream before saving.", "validationTitle": "Stream Validation", "connectAllStreams": "Connect All Streams", @@ -262,6 +311,8 @@ "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", @@ -272,10 +323,15 @@ "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." + }, "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." }, @@ -385,7 +441,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": { @@ -443,7 +500,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." }, "inertia": { "title": "Inertia", @@ -731,7 +788,8 @@ "createRole": "Role {{role}} created successfully", "updateCameras": "Cameras updated for role {{role}}", "deleteRole": "Role {{role}} deleted successfully", - "userRolesUpdated": "{{count}} user(s) assigned to this role have been updated to 'viewer', which has access to all cameras." + "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}}", @@ -875,7 +933,7 @@ "desc": "Semantic Search must be enabled to use Triggers." }, "management": { - "title": "Trigger 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", @@ -895,8 +953,9 @@ "description": "Description" }, "actions": { - "alert": "Mark as Alert", - "notification": "Send Notification" + "notification": "Send Notification", + "sub_label": "Add Sub Label", + "attribute": "Add Attribute" }, "dialog": { "createTrigger": { @@ -914,10 +973,11 @@ "form": { "name": { "title": "Name", - "placeholder": "Enter trigger name", + "placeholder": "Name this trigger", + "description": "Enter a unique name or description to identify this trigger", "error": { - "minLength": "Name must be at least 2 characters long.", - "invalidCharacters": "Name can only contain letters, numbers, underscores, and hyphens.", + "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." } }, @@ -926,18 +986,15 @@ }, "type": { "title": "Type", - "placeholder": "Select trigger type" - }, - "friendly_name": { - "title": "Friendly Name", - "placeholder": "Name or describe this trigger", - "description": "An optional friendly name or descriptive text for this trigger." + "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 an image", + "imagePlaceholder": "Select a thumbnail", "textPlaceholder": "Enter text content", - "imageDesc": "Select an image to trigger this action when a similar image is detected.", + "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." @@ -945,6 +1002,7 @@ }, "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" @@ -952,13 +1010,30 @@ }, "actions": { "title": "Actions", - "desc": "By default, Frigate fires an MQTT message for all triggers. Choose an additional action to perform when this trigger fires.", + "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.", diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index c4c6fd4f6..73c6d65b5 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -76,7 +76,12 @@ } }, "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", @@ -169,6 +174,7 @@ "enrichments": { "title": "Enrichments", "infPerSecond": "Inferences Per Second", + "averageInf": "Average Inference Time", "embeddings": { "image_embedding": "Image Embedding", "text_embedding": "Text Embedding", @@ -180,7 +186,13 @@ "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" } } } diff --git a/web/public/locales/es/common.json b/web/public/locales/es/common.json index 3c682b706..9f35ee958 100644 --- a/web/public/locales/es/common.json +++ b/web/public/locales/es/common.json @@ -280,5 +280,8 @@ "desc": "Página no encontrada" }, "selectItem": "Seleccionar {{item}}", - "readTheDocumentation": "Leer la documentación" + "readTheDocumentation": "Leer la documentación", + "information": { + "pixels": "{{area}}px" + } } 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/dialog.json b/web/public/locales/es/components/dialog.json index 689b4fcc1..e200c388d 100644 --- a/web/public/locales/es/components/dialog.json +++ b/web/public/locales/es/components/dialog.json @@ -69,7 +69,7 @@ "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." }, "fromTimeline": { "saveExport": "Guardar exportación", @@ -120,7 +120,8 @@ "button": { "export": "Exportar", "markAsReviewed": "Marcar como revisado", - "deleteNow": "Eliminar ahora" + "deleteNow": "Eliminar ahora", + "markAsUnreviewed": "Marcar como no revisado" } }, "imagePicker": { diff --git a/web/public/locales/es/views/classificationModel.json b/web/public/locales/es/views/classificationModel.json new file mode 100644 index 000000000..b2446ea01 --- /dev/null +++ b/web/public/locales/es/views/classificationModel.json @@ -0,0 +1,40 @@ +{ + "documentTitle": "Modelos de Clasificación", + "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." + }, + "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": "Fallo al iniciar el entrenamiento del modelo: {{errorMessage}}", + "updateModelFailed": "Fallo al actualizar modelo: {{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." + }, + "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." + } +} diff --git a/web/public/locales/es/views/events.json b/web/public/locales/es/views/events.json index a428169a4..097b08b64 100644 --- a/web/public/locales/es/views/events.json +++ b/web/public/locales/es/views/events.json @@ -37,5 +37,17 @@ "selected_other": "{{count}} seleccionados", "detected": "detectado", "suspiciousActivity": "Actividad Sospechosa", - "threateningActivity": "Actividad Amenzadora" + "threateningActivity": "Actividad Amenzadora", + "zoomIn": "Agrandar", + "zoomOut": "Alejar", + "detail": { + "label": "Detalle", + "trackedObject_one": "objeto", + "trackedObject_other": "objetos", + "noObjectDetailData": "No hay datos detallados del objeto.", + "settings": "Configuración de la Vista Detalle" + }, + "objectTrack": { + "clickToSeek": "Clic para ir a este momento" + } } diff --git a/web/public/locales/es/views/explore.json b/web/public/locales/es/views/explore.json index 4816a78ed..064bdf0d8 100644 --- a/web/public/locales/es/views/explore.json +++ b/web/public/locales/es/views/explore.json @@ -110,7 +110,8 @@ "snapshot": "captura instantánea", "video": "vídeo", "object_lifecycle": "ciclo de vida del objeto", - "details": "detalles" + "details": "detalles", + "thumbnail": "miniatura" }, "objectLifecycle": { "title": "Ciclo de vida del objeto", @@ -224,5 +225,11 @@ }, "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" } } 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..25fa983e7 100644 --- a/web/public/locales/es/views/faceLibrary.json +++ b/web/public/locales/es/views/faceLibrary.json @@ -1,6 +1,6 @@ { "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." }, @@ -23,11 +23,11 @@ "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", + "title": "Reconocimientos Recientes", + "aria": "Seleccionar reconocimientos recientes", "empty": "No hay intentos recientes de reconocimiento facial" }, "selectItem": "Seleccionar {{item}}", @@ -49,7 +49,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": { diff --git a/web/public/locales/es/views/live.json b/web/public/locales/es/views/live.json index 5f567de06..2109cbb28 100644 --- a/web/public/locales/es/views/live.json +++ b/web/public/locales/es/views/live.json @@ -147,7 +147,7 @@ "snapshots": "Capturas de pantalla", "autotracking": "Seguimiento automático", "cameraEnabled": "Cámara habilitada", - "transcription": "Transcripción de audio" + "transcription": "Transcripción de Audio" }, "history": { "label": "Mostrar grabaciones históricas" @@ -170,5 +170,10 @@ "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.", + "buttonText": "Añade Cámara" } } diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json index bde0d99ab..42bead0be 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -10,7 +10,9 @@ "general": "Configuración General - 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", @@ -23,7 +25,10 @@ "users": "Usuarios", "notifications": "Notificaciones", "enrichments": "Análisis avanzado", - "triggers": "Disparadores" + "triggers": "Disparadores", + "roles": "Rols", + "cameraManagement": "Administración", + "cameraReview": "Revisar" }, "dialog": { "unsavedChanges": { @@ -773,7 +778,71 @@ "desc": "Editar configuractión del disparador para cámara {{camera}}" }, "deleteTrigger": { - "title": "Eliminar Disparador" + "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": "Entre nombre de disparador", + "error": { + "minLength": "El nombre debe tener al menos 2 caracteres.", + "invalidCharacters": "El nombre sólo puede contener letras, números, guiones bajos, y guiones.", + "alreadyExists": "Un disparador con este nombre ya existe para esta cámara." + } + }, + "enabled": { + "description": "Activa o desactiva este disparador" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleccione tipo de disparador" + }, + "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 imágen", + "textPlaceholder": "Entre contenido de texto", + "error": { + "required": "El contenido es requrido." + }, + "imageDesc": "Seleccione una imágen para iniciar esta acción cuando una imágen similar es detectada.", + "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" + } + }, + "actions": { + "title": "Acciones", + "error": { + "min": "Al menos una acción debe ser seleccionada." + }, + "desc": "Por defecto, Frigate manda un mensaje MQTT por todos los disparadores. Seleccione una acción adicional que se realizará cuando este disparador se accione." + } + } + }, + "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}}" } } }, @@ -796,7 +865,9 @@ "createRole": "Rol {{role}} creado exitosamente", "updateCameras": "Cámara actualizada para el rol {{role}}", "deleteRole": "Rol {{role}} eliminado exitosamente", - "userRolesUpdated": "{{count}} usuarios asignados a este rol han sido actualizados a 'visor', que tiene acceso a todas las cámaras." + "userRolesUpdated_one": "{{count}} usuarios asignados a este rol han sido actualizados a 'visor', que tiene acceso a todas las cámaras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Creación de rol fallida: {{errorMessage}}", diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json index 5b407bb91..e54a7802b 100644 --- a/web/public/locales/es/views/system.json +++ b/web/public/locales/es/views/system.json @@ -105,7 +105,7 @@ "unusedStorageInformation": "Información de Almacenamiento No Utilizado" }, "shm": { - "title": "Asignación SHM (memoria compartida)", + "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." } }, diff --git a/web/public/locales/fa/views/classificationModel.json b/web/public/locales/fa/views/classificationModel.json new file mode 100644 index 000000000..a4bdd37e4 --- /dev/null +++ b/web/public/locales/fa/views/classificationModel.json @@ -0,0 +1,22 @@ +{ + "button": { + "deleteClassificationAttempts": "حذف تصاویر طبقه بندی", + "renameCategory": "تغییر نام کلاس", + "deleteCategory": "حذف کردن کلاس", + "deleteImages": "حذف کردن عکس ها", + "trainModel": "مدل آموزش" + }, + "toast": { + "success": { + "deletedCategory": "کلاس حذف شده", + "deletedImage": "عکس های حذف شده", + "categorizedImage": "تصویر طبقه بندی شده", + "trainedModel": "مدل آموزش دیده شده.", + "trainingModel": "آموزش دادن مدل با موفقیت شروع شد." + }, + "error": { + "deleteImageFailed": "حذف نشد:{{پیغام خطا}}", + "deleteCategoryFailed": "کلاس حذف نشد:{{پیغام خطا}}" + } + } +} diff --git a/web/public/locales/fa/views/explore.json b/web/public/locales/fa/views/explore.json index f98ef706f..8cbff2582 100644 --- a/web/public/locales/fa/views/explore.json +++ b/web/public/locales/fa/views/explore.json @@ -1,3 +1,4 @@ { - "generativeAI": "هوش مصنوعی تولید کننده" + "generativeAI": "هوش مصنوعی تولید کننده", + "documentTitle": "کاوش کردن - فرایگیت" } 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/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 960dd1d0d..3cf25e977 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,23 +65,23 @@ }, "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": { @@ -98,16 +98,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,7 +120,7 @@ "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", @@ -142,14 +142,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)", @@ -163,14 +163,14 @@ "yue": "粵語 (Cantonais)", "th": "ไทย (Thai)", "ca": "Català (Catalan)", - "ptBR": "Português brasileiro (portugais brésilien)", + "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)" + "sl": "Slovenščina (Slovène)", + "lt": "Lietuvių (Lithuanien)", + "bg": "Български (Bulgare)", + "gl": "Galego (Galicien)", + "id": "Bahasa Indonesia (Indonésien)", + "ur": "اردو (Ourdou)" }, "appearance": "Apparence", "darkMode": { @@ -181,7 +181,7 @@ }, "label": "Mode sombre" }, - "review": "Revue d'événements", + "review": "Événements", "explore": "Explorer", "export": "Exporter", "user": { @@ -199,18 +199,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", @@ -224,7 +224,7 @@ "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" }, @@ -233,16 +233,16 @@ "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 événements, et à l'historique des enregistrements dans l'interface." }, "pagination": { "next": { @@ -266,10 +266,13 @@ "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" }, "unit": { "speed": { @@ -279,6 +282,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 8595697bc..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,32 +40,32 @@ } }, "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" } @@ -80,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 111baf88c..771903663 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,25 @@ "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." }, "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 +88,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,32 +102,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 à cet événement ?

    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 événements sélectionnés 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 vérifié", + "deleteNow": "Supprimer maintenant", + "markAsUnreviewed": "Marquer comme non vérifié" } }, "imagePicker": { - "selectImage": "Sélectionnez une vignette d'objet suivi", + "selectImage": "Sélectionnez une vignette d'objet suivi.", "search": { - "placeholder": "Chercher par libellé ou sous-libellé..." + "placeholder": "Rechercher par étiquette ou sous-étiquette" }, - "noImages": "Aucune vignette trouvée pour cette caméra" + "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 58a03de93..68a25f950 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 éléments vérifiés" }, "cameras": { "label": "Filtre des caméras", @@ -101,27 +101,27 @@ "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 effacer" + "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" 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..19d18f385 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 de votre événement 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..7d7d93ba3 --- /dev/null +++ b/web/public/locales/fr/views/classificationModel.json @@ -0,0 +1,164 @@ +{ + "documentTitle": "Modèles de classification", + "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" + }, + "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": "Échec du démarrage de l'entraînement du modèle : {{errorMessage}}", + "deleteModelFailed": "Impossible de supprimer le modèle : {{errorMessage}}", + "updateModelFailed": "Impossible de mettre à jour le 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." + }, + "deleteDatasetImages": { + "title": "Supprimer les images du jeu de données", + "desc": "Ê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": "Ê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." + }, + "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" + } + }, + "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": "Voulez-vous vraiment supprimer {{count}} modèle(s) ? 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." + }, + "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." + } +} diff --git a/web/public/locales/fr/views/configEditor.json b/web/public/locales/fr/views/configEditor.json index ce4c3292e..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": { diff --git a/web/public/locales/fr/views/events.json b/web/public/locales/fr/views/events.json index cf4f5e142..c32ffec3c 100644 --- a/web/public/locales/fr/views/events.json +++ b/web/public/locales/fr/views/events.json @@ -2,22 +2,22 @@ "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", + "alert": "Il n'y a aucune alerte à examiner.", + "detection": "Il n'y a aucune détection à examiner.", "motion": "Aucune donnée de mouvement trouvée" }, "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": "Événements - Frigate", "recordings": { "documentTitle": "Enregistrements - Frigate" }, @@ -25,17 +25,36 @@ "last24Hours": "Dernières 24 heures" }, "timeline.aria": "Sélectionner une chronologie", - "markAsReviewed": "Marqué comme passé en revue", + "markAsReviewed": "Marquer comme vérifié", "newReviewItems": { - "button": "Nouveaux éléments à passer en revue", - "label": "Afficher les nouveaux éléments de la revue d'événements" + "button": "Nouveaux événements à examiner", + "label": "Afficher les nouveaux événements" }, "camera": "Caméra", - "markTheseItemsAsReviewed": "Marquer ces éléments comme passés en revue", + "markTheseItemsAsReviewed": "Marquer ces éléments comme vérifiés", "selected": "{{count}} sélectionné(s)", "selected_other": "{{count}} sélectionné(s)", "selected_one": "{{count}} sélectionné(s)", "detected": "détecté", "suspiciousActivity": "Activité suspecte", - "threateningActivity": "Activité menaçante" + "threateningActivity": "Activité menaçante", + "detail": { + "noDataFound": "Aucun détail à examiner", + "aria": "Activer/désactiver la vue détaillée", + "trackedObject_one": "objet", + "trackedObject_other": "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 de l'événement actif si disponibles" + } + }, + "objectTrack": { + "trackedPoint": "Point suivi", + "clickToSeek": "Cliquez pour atteindre ce moment." + }, + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière" } diff --git a/web/public/locales/fr/views/explore.json b/web/public/locales/fr/views/explore.json index 064a71a37..015d7560b 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,50 +24,50 @@ }, "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'événement", "button": { - "share": "Partager cet élément de la revue d'événements", + "share": "Partager cet événement", "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.", - "audioTranscription": "Requête de la transcription audio réussie." + "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" }, "error": { "regenerate": "Échec de l'appel de {{provider}} pour une nouvelle description : {{errorMessage}}", - "updatedSublabelFailed": "Échec de la mise à jour du sous-libellé : {{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 requête de transcription audio : {{errorMessage}}" + "audioTranscription": "Échec de la demande de transcription audio : {{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 cet événement. 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 événement. 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 événement. 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'événement" }, - "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": { @@ -86,8 +86,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", @@ -109,7 +109,8 @@ "details": "détails", "video": "vidéo", "object_lifecycle": "cycle de vie de l'objet", - "snapshot": "instantané" + "snapshot": "instantané", + "thumbnail": "Miniature" }, "objectLifecycle": { "title": "Cycle de vie de l'objet", @@ -120,8 +121,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}}" @@ -129,7 +130,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", @@ -139,7 +140,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": { @@ -175,8 +176,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", @@ -196,12 +197,22 @@ "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" } }, "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é", @@ -223,6 +234,54 @@ "title": "Analyse IA" }, "concerns": { - "label": "Préoccupations" + "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" + } + }, + "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": "ASTUCE : Imaginez une séquence d'événement avec une personne marchant de gauche à droite. Si le cadre de détection sur la chronologie de l'événement est constamment à gauche de la personne, la valeur doit être diminuée. De même, si une personne marche de gauche à droite et que le cadre de détection est constamment devant la personne, la valeur doit être augmentée.", + "toast": { + "success": "Le décalage des annotations pour {{camera}} a été sauvegardé dans le fichier de configuration. Redémarrez Frigate pour appliquer vos modifications." + }, + "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 618185d23..7cdfb6c88 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,18 @@ "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." }, "selectFace": "Sélectionner un visage", "button": { @@ -41,12 +41,12 @@ }, "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", + "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 +58,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 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 +89,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 +98,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 0ced5dc1a..39cebdd02 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,18 @@ }, "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" @@ -79,51 +79,51 @@ }, "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", "debug": { @@ -141,7 +141,7 @@ "transcription": "Transcription audio" }, "history": { - "label": "Afficher l'historique de capture" + "label": "Afficher les vidéos archivées" }, "effectiveRetainMode": { "modes": { @@ -165,10 +165,21 @@ "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" + }, + "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..8b76ebe5d 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,19 +13,19 @@ "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é", + "recognized_license_plate": "Plaque d'immatriculation reconnue", + "has_clip": "Avec une séquence vidéo", + "has_snapshot": "Avec un instantané", "max_score": "Score maximum" }, "searchType": { @@ -34,11 +34,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 +54,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 6a3e61462..61202dc6f 100644 --- a/web/public/locales/fr/views/settings.json +++ b/web/public/locales/fr/views/settings.json @@ -4,26 +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", + "motionTuner": "Réglage de la détection de mouvement - Frigate", "general": "Paramètres généraux - Frigate", "masksAndZones": "Éditeur de masques et de zones - Frigate", "object": "Débogage - Frigate", "frigatePlus": "Paramètres Frigate+ - Frigate", "notifications": "Paramètres de notification - Frigate", - "enrichments": "Paramètres d'enrichissements - Frigate" + "enrichments": "Paramètres d'enrichissements - Frigate", + "cameraManagement": "Gestion des caméras - Frigate", + "cameraReview": "Paramètres des événements de 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": "Enrichissements", - "triggers": "Déclencheurs" + "triggers": "Déclencheurs", + "roles": "Rôles", + "cameraManagement": "Gestion", + "cameraReview": "Événements" }, "dialog": { "unsavedChanges": { @@ -44,8 +49,12 @@ "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" } }, "storedLayouts": { @@ -62,13 +71,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": "Le jour du début de la semaine du calendrier des événements", "sunday": "Dimanche", "monday": "Lundi" }, @@ -127,7 +136,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", @@ -137,12 +146,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+.", @@ -151,12 +160,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+." }, @@ -167,7 +176,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", @@ -183,7 +192,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", @@ -327,7 +336,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": { @@ -351,7 +361,7 @@ }, "snapPoints": { "true": "Points d'accrochage", - "false": "Ne cassez pas les points" + "false": "Ne pas réunir les points" } }, "loiteringTime": { @@ -366,7 +376,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." } } }, @@ -380,12 +390,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." }, "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", @@ -411,7 +421,7 @@ "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": { @@ -426,7 +436,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." @@ -454,7 +464,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." @@ -476,7 +486,7 @@ "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": { @@ -498,7 +508,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", @@ -521,12 +531,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", @@ -543,11 +553,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": { @@ -564,9 +574,9 @@ "detectorDesc": "Frigate utilise vos détecteurs ({{detectors}}) pour détecter les objets dans le flux vidéo de votre caméra.", "desc": "La vue de débogage affiche en temps réel les objets suivis et leurs statistiques. La liste des objets affiche un résumé différé des objets détectés.", "paths": { - "title": "Chemins", - "desc": "Montrer les points notables du chemin de l'objet suivi", - "tips": "

    Chemins


    Les lignes et les cercles indiqueront les points notables des déplacements de l'objet suivi pendant son cycle de vie.

    " + "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", @@ -575,7 +585,7 @@ "currentRMS": "RMS actuel", "currentdbFS": "dbFS actuel" }, - "openCameraWebUI": "Ouvre l'interface Web de {{camera}}" + "openCameraWebUI": "Ouvrir l'interface Web de {{camera}}" }, "users": { "title": "Utilisateurs", @@ -612,34 +622,34 @@ "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" } }, "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", + "usernameIsRequired": "Nom d'utilisateur requis", "passwordIsRequired": "Mot de passe requis" }, "deleteUser": { @@ -649,7 +659,7 @@ }, "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" @@ -662,7 +672,7 @@ "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, Événements, Explorer et Exports.", "customDesc": "Rôle personnalisé avec accès spécifique à la caméra" }, "select": "Sélectionnez un rôle" @@ -670,7 +680,7 @@ "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" } } @@ -679,26 +689,26 @@ "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", @@ -706,7 +716,7 @@ }, "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 vous permet de retrouver les objets suivis dans vos é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." }, "unsavedChanges": "Modifications non enregistrées des paramètres d'enrichissements", "faceRecognition": { @@ -714,33 +724,33 @@ "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 d'enrichissements 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 d'enrichissements modifiés)" }, "triggers": { "documentTitle": "Déclencheurs", "management": { - "title": "Gestion des déclencheurs", + "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", @@ -751,7 +761,7 @@ "threshold": "Seuil", "actions": "Actions", "noTriggers": "Aucun déclencheur configuré pour cette caméra.", - "edit": "Éditer", + "edit": "Modifier", "deleteTrigger": "Supprimer le déclencheur", "lastTriggered": "Dernier déclencheur" }, @@ -761,7 +771,9 @@ }, "actions": { "alert": "Marquer comme alerte", - "notification": "Envoyer une notification" + "notification": "Envoyer une notification", + "sub_label": "Ajouter une sous-étiquette", + "attribute": "Ajouter un attribut" }, "dialog": { "createTrigger": { @@ -769,8 +781,8 @@ "desc": "Créer un déclencheur pour la caméra {{camera}}" }, "editTrigger": { - "title": "Éditer le déclencheur", - "desc": "Éditer les paramètres du déclencheur de la caméra {{camera}}" + "title": "Modifier le déclencheur", + "desc": "Modifier les paramètres du déclencheur de la caméra {{camera}}" }, "deleteTrigger": { "title": "Supprimer le déclencheur", @@ -779,25 +791,28 @@ "form": { "name": { "title": "Nom", - "placeholder": "Entrez le nom du déclencheur", + "placeholder": "Nommez ce déclencheur", "error": { - "minLength": "Le nom soit comporter au moins deux caractères.", - "invalidCharacters": "Le nom peut contenir uniquement des lettres, des nombres, des tirets bas, et des tirets.", + "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" + "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 image", + "imagePlaceholder": "Sélectionner une vignette", "textPlaceholder": "Saisir le contenu du texte", - "imageDesc": "Sélectionnez une image pour déclencher cette action lorsqu'une image similaire est détectée.", + "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." @@ -808,14 +823,20 @@ "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 publie un message MQTT à chaque déclenchement. Sélectionnez une action complémentaire à exécuter lors de ce déclenchement.", + "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." } } }, @@ -830,12 +851,33 @@ "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 de visionnage", - "desc": "Gérer les rôles personnalisés de visionnage et leurs permissions d'accès aux caméras pour cette instance de Frigate." + "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": { @@ -843,7 +885,7 @@ "cameras": "Caméras", "actions": "Actions", "noRoles": "Aucun rôle personnalisé trouvé.", - "editCameras": "Éditer les caméras", + "editCameras": "Modifier les caméras", "deleteRole": "Supprimer le rôle" }, "toast": { @@ -851,7 +893,9 @@ "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": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits 'visionnage', et ont accès à toutes les caméras." + "userRolesUpdated_one": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Échec dans la création du rôle : {{errorMessage}}", @@ -866,22 +910,22 @@ "desc": "Ajouter un nouveau rôle et définir les permissions d'accès à la caméra." }, "editCameras": { - "title": "Editer les caméras du rôle", + "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 'visionnage', avec un accès à toutes les caméras.", + "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, nombres, points et tirets bas sont autorisés.", + "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 peut uniquement contenir des lettres, nombres, . ou _", + "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": { @@ -891,5 +935,231 @@ } } } + }, + "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" + }, + "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 testez la connexion.", + "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..." + } + }, + "step2": { + "description": "Définissez les rôles du flux et ajoutez des flux supplémentaires pour votre caméra.", + "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 du flux réussi !", + "testFailed": "Échec du test du flux", + "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." + } + }, + "step3": { + "description": "Validation et analyse finales avant l'enregistrement de votre nouvelle caméra. Connectez chaque flux avant d'enregistrer.", + "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." + } + }, + "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 événements de la 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 événements 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 par l'IA ne sera générée pour les événements de cette caméra." + }, + "review": { + "title": "Événements", + "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, aucun nouvel événement n'est généré. ", + "alerts": "Alertes ", + "detections": "Détections " + }, + "reviewClassification": { + "title": "Classification des événements", + "desc": "Frigate classe les événements 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 événements 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 événements 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 cf9c1699e..9b432ba7f 100644 --- a/web/public/locales/fr/views/system.json +++ b/web/public/locales/fr/views/system.json @@ -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,23 +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", - "cpuUsageInformation": "Utilisation du processeur pour préparer les données avant et après leur passage dans les modèles de détection. Cette valeur ne mesure pas les calculs d'inférence, même si un GPU ou un autre accélérateur est utilisé." + "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", @@ -66,22 +66,22 @@ "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" }, "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" } }, "storage": { @@ -89,78 +89,78 @@ "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", "shm": { "title": "Allocation de mémoire partagée SHM", - "warning": "La taille actuelle de la SHM de {{total}} Mo est trop faible. Augmentez la au moins à {{min_shm}} Mo." + "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'inspecter 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}} a une utilisation CPU de 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)", @@ -170,16 +170,16 @@ "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", + "text_embedding": "Vitesse d'embedding de visage", "yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9" } } 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/he/audio.json b/web/public/locales/he/audio.json index f7369853c..841dfa83b 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": "סיטאר", @@ -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,54 @@ "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": "לְגַרֵד" } diff --git a/web/public/locales/he/components/filter.json b/web/public/locales/he/components/filter.json index 2316722ca..17f7914cf 100644 --- a/web/public/locales/he/components/filter.json +++ b/web/public/locales/he/components/filter.json @@ -1,5 +1,5 @@ { - "filter": "לסנן", + "filter": "מסנן", "features": { "submittedToFrigatePlus": { "tips": "עליך תחילה לסנן לפי אובייקטים במעקב שיש להם תמונת מצב.

    לא ניתן לשלוח ל-Frigate+ אובייקטים במעקב ללא תמונת מצב.", @@ -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,26 @@ "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}} מחלקות" } } diff --git a/web/public/locales/he/views/classificationModel.json b/web/public/locales/he/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/he/views/classificationModel.json @@ -0,0 +1 @@ +{} 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..fbccbeeb2 100644 --- a/web/public/locales/he/views/events.json +++ b/web/public/locales/he/views/events.json @@ -34,5 +34,16 @@ "selected_one": "נבחרו {{count}}", "selected_other": "{{count}} נבחרו", "camera": "מצלמה", - "detected": "זוהה" + "detected": "זוהה", + "detail": { + "noDataFound": "אין נתונים מפורטים לבדיקה", + "aria": "הפעלה/כיבוי תצוגת פרטים", + "trackedObject_one": "אובייקט במעקב", + "trackedObject_other": "אובייקטים במעקב", + "noObjectDetailData": "אין נתוני אובייקט זמינים." + }, + "objectTrack": { + "trackedPoint": "נקודה במעקב", + "clickToSeek": "לחץ כדי לחפש את הזמן הזה" + } } diff --git a/web/public/locales/he/views/live.json b/web/public/locales/he/views/live.json index 9fccbd158..7b7c53569 100644 --- a/web/public/locales/he/views/live.json +++ b/web/public/locales/he/views/live.json @@ -63,7 +63,15 @@ "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה הממונעת" } }, - "presets": "מצלמה ממונעת - פריסטים" + "presets": "מצלמה ממונעת - פריסטים", + "focus": { + "in": { + "label": "כניסת פוקוס מצלמת PTZ" + }, + "out": { + "label": "יציאת פוקוס מצלמת PTZ" + } + } }, "camera": { "enable": "אפשור מצלמה", 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/settings.json b/web/public/locales/he/views/settings.json index 1ab0ab917..e0737aa6e 100644 --- a/web/public/locales/he/views/settings.json +++ b/web/public/locales/he/views/settings.json @@ -268,7 +268,9 @@ "notifications": "הגדרת התראות - Frigate", "authentication": "הגדרות אימות - Frigate", "default": "הגדרות - Frigate", - "general": "הגדרות כלליות - Frigate" + "general": "הגדרות כלליות - Frigate", + "cameraManagement": "ניהול מצלמות - Frigate", + "cameraReview": "הגדרות סקירת מצלמה - Frigate" }, "menu": { "ui": "UI - ממשק משתמש", @@ -280,7 +282,10 @@ "notifications": "התראות", "frigateplus": "+Frigate", "enrichments": "תוספות", - "triggers": "הפעלות" + "triggers": "הפעלות", + "cameraManagement": "ניהול", + "cameraReview": "סְקִירָה", + "roles": "תפקידים" }, "dialog": { "unsavedChanges": { diff --git a/web/public/locales/he/views/system.json b/web/public/locales/he/views/system.json index 4e21f1a0a..d30f9437e 100644 --- a/web/public/locales/he/views/system.json +++ b/web/public/locales/he/views/system.json @@ -52,7 +52,8 @@ "inferenceSpeed": "מהירות זיהוי", "temperature": "טמפרטורת הגלאי", "cpuUsage": "ניצול מעבד על ידי הגלאי", - "memoryUsage": "שימוש בזיכרון על ידי הגלאי" + "memoryUsage": "שימוש בזיכרון על ידי הגלאי", + "cpuUsageInformation": "המעבד המשמש להכנת נתוני קלט ופלט אל/ממודלי זיהוי. ערך זה אינו מודד את השימוש בהסקה, גם אם נעשה שימוש במעבד גרפי או מאיץ." }, "hardwareInfo": { "gpuMemory": "זיכרון GPU", 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/hr/views/classificationModel.json b/web/public/locales/hr/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/hr/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/hu/common.json b/web/public/locales/hu/common.json index f75193fd1..99e0450c2 100644 --- a/web/public/locales/hu/common.json +++ b/web/public/locales/hu/common.json @@ -176,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." }, @@ -221,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": { @@ -263,5 +271,8 @@ "label": { "back": "Vissza" }, - "readTheDocumentation": "Olvassa el a dokumentációt" + "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/dialog.json b/web/public/locales/hu/components/dialog.json index 8a6a452ed..c45eac1fc 100644 --- a/web/public/locales/hu/components/dialog.json +++ b/web/public/locales/hu/components/dialog.json @@ -4,7 +4,7 @@ "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" } }, @@ -107,7 +107,8 @@ "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": { diff --git a/web/public/locales/hu/views/classificationModel.json b/web/public/locales/hu/views/classificationModel.json new file mode 100644 index 000000000..8ce7864e9 --- /dev/null +++ b/web/public/locales/hu/views/classificationModel.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Osztályozási modellek", + "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" + }, + "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" + } + } +} diff --git a/web/public/locales/hu/views/events.json b/web/public/locales/hu/views/events.json index d470abbd5..abea6b464 100644 --- a/web/public/locales/hu/views/events.json +++ b/web/public/locales/hu/views/events.json @@ -36,5 +36,6 @@ "selected_one": "{{count}} kiválasztva", "selected_other": "{{count}} kiválasztva", "suspiciousActivity": "Gyanús Tevékenység", - "threateningActivity": "Fenyegető Tevékenység" + "threateningActivity": "Fenyegető Tevékenység", + "zoomIn": "Nagyítás" } 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 7b12521d5..4f9331f87 100644 --- a/web/public/locales/hu/views/faceLibrary.json +++ b/web/public/locales/hu/views/faceLibrary.json @@ -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": { diff --git a/web/public/locales/hu/views/live.json b/web/public/locales/hu/views/live.json index 04f0836f0..b7a5ff967 100644 --- a/web/public/locales/hu/views/live.json +++ b/web/public/locales/hu/views/live.json @@ -134,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": { @@ -167,5 +170,16 @@ "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/settings.json b/web/public/locales/hu/views/settings.json index 29f6ba33f..c36e9a53f 100644 --- a/web/public/locales/hu/views/settings.json +++ b/web/public/locales/hu/views/settings.json @@ -10,7 +10,9 @@ "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", @@ -23,7 +25,10 @@ "notifications": "Értesítések", "frigateplus": "Frigate+", "enrichments": "Extra funkciók", - "triggers": "Triggerek" + "triggers": "Triggerek", + "roles": "Szerepkörök", + "cameraManagement": "Menedzsment", + "cameraReview": "Vizsgálat" }, "dialog": { "unsavedChanges": { @@ -254,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", @@ -317,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" }, @@ -749,6 +755,11 @@ "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." } } }, @@ -763,6 +774,80 @@ "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/id/audio.json b/web/public/locales/id/audio.json index d065bf137..0f759c193 100644 --- a/web/public/locales/id/audio.json +++ b/web/public/locales/id/audio.json @@ -81,5 +81,9 @@ "electric_guitar": "Gitar Elektrik", "acoustic_guitar": "Gitar Akustik", "strum": "Genjreng", - "banjo": "Banjo" + "banjo": "Banjo", + "snoring": "Ngorok", + "cough": "Batuk", + "clapping": "Tepukan", + "camera": "Kamera" } 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/views/classificationModel.json b/web/public/locales/id/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/id/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/id/views/configEditor.json b/web/public/locales/id/views/configEditor.json index 3577999ab..a4d7baeaa 100644 --- a/web/public/locales/id/views/configEditor.json +++ b/web/public/locales/id/views/configEditor.json @@ -13,5 +13,6 @@ } }, "confirm": "Keluar tanpa menyimpan?", - "safeModeDescription": "Frigate sedang dalam mode aman karena kesalahan validasi konfigurasi." + "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/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/settings.json b/web/public/locales/id/views/settings.json index 43c59244e..8d1b4dec8 100644 --- a/web/public/locales/id/views/settings.json +++ b/web/public/locales/id/views/settings.json @@ -8,6 +8,11 @@ "motionTuner": "Penyetel Gerakan - Frigate", "general": "Frigate - Pengaturan Umum", "object": "Debug - Frigate", - "enrichments": "Frigate - Pengaturan Pengayaan" + "enrichments": "Frigate - Pengaturan Pengayaan", + "cameraManagement": "Pengaturan Kamera - Frigate" + }, + "menu": { + "cameraManagement": "Pengaturan", + "notifications": "Notifikasi" } } diff --git a/web/public/locales/it/audio.json b/web/public/locales/it/audio.json index 9a08719ad..caf48582b 100644 --- a/web/public/locales/it/audio.json +++ b/web/public/locales/it/audio.json @@ -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 f4ee9086f..48c37740c 100644 --- a/web/public/locales/it/common.json +++ b/web/public/locales/it/common.json @@ -134,10 +134,21 @@ "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" }, "menu": { "configuration": "Configurazione", @@ -257,7 +268,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.", @@ -280,5 +291,17 @@ } }, "selectItem": "Seleziona {{item}}", - "readTheDocumentation": "Leggi la documentazione" + "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/dialog.json b/web/public/locales/it/components/dialog.json index f5a124779..0f69e7213 100644 --- a/web/public/locales/it/components/dialog.json +++ b/web/public/locales/it/components/dialog.json @@ -61,7 +61,7 @@ "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", @@ -110,7 +110,8 @@ "button": { "export": "Esporta", "markAsReviewed": "Segna come visto", - "deleteNow": "Elimina ora" + "deleteNow": "Elimina ora", + "markAsUnreviewed": "Segna come non visto" }, "confirmDelete": { "desc": { @@ -128,6 +129,7 @@ "search": { "placeholder": "Cerca per etichetta o sottoetichetta..." }, - "noImages": "Nessuna miniatura trovata per questa fotocamera" + "noImages": "Nessuna miniatura trovata per questa fotocamera", + "unknownLabel": "Immagine di attivazione salvata" } } diff --git a/web/public/locales/it/components/filter.json b/web/public/locales/it/components/filter.json index 85dabf7e7..22fc52093 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" } } diff --git a/web/public/locales/it/views/classificationModel.json b/web/public/locales/it/views/classificationModel.json new file mode 100644 index 000000000..343cc3602 --- /dev/null +++ b/web/public/locales/it/views/classificationModel.json @@ -0,0 +1,164 @@ +{ + "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" + }, + "error": { + "deleteImageFailed": "Impossibile eliminare: {{errorMessage}}", + "deleteCategoryFailed": "Impossibile eliminare la classe: {{errorMessage}}", + "categorizeFailed": "Impossibile categorizzare l'immagine: {{errorMessage}}", + "trainingFailed": "Impossibile avviare l'addestramento del modello: {{errorMessage}}", + "deleteModelFailed": "Impossibile eliminare il modello: {{errorMessage}}", + "updateModelFailed": "Impossibile aggiornare il modello: {{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." + }, + "deleteDatasetImages": { + "title": "Elimina immagini della base dati", + "desc": "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": "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" + }, + "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" + } + }, + "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": "Vuoi davvero eliminare {{count}} modello/i? 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." + }, + "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." + } +} diff --git a/web/public/locales/it/views/events.json b/web/public/locales/it/views/events.json index 791bcc135..c5c90509f 100644 --- a/web/public/locales/it/views/events.json +++ b/web/public/locales/it/views/events.json @@ -37,5 +37,24 @@ "selected_other": "{{count}} selezionati", "detected": "rilevato", "suspiciousActivity": "Attività sospetta", - "threateningActivity": "Attività minacciosa" + "threateningActivity": "Attività minacciosa", + "detail": { + "noDataFound": "Nessun dato dettagliato da rivedere", + "aria": "Attiva/disattiva la visualizzazione dettagliata", + "trackedObject_one": "oggetto", + "trackedObject_other": "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" } diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json index a440e0281..cbe20ab4f 100644 --- a/web/public/locales/it/views/explore.json +++ b/web/public/locales/it/views/explore.json @@ -158,7 +158,8 @@ "snapshot": "istantanea", "object_lifecycle": "ciclo di vita dell'oggetto", "details": "dettagli", - "video": "video" + "video": "video", + "thumbnail": "miniatura" }, "itemMenu": { "downloadSnapshot": { @@ -195,11 +196,21 @@ "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" } }, "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" } }, @@ -224,5 +235,53 @@ }, "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" + } + }, + "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": "SUGGERIMENTO: Immagina un video evento con una persona che cammina da sinistra a destra. Se il riquadro di delimitazione della cronologia dell'evento si trova costantemente a sinistra della persona, il valore dovrebbe essere diminuito. Allo stesso modo, se una persona cammina da sinistra a destra e il riquadro di delimitazione si trova costantemente davanti alla persona, il valore dovrebbe essere aumentato.", + "toast": { + "success": "La differenza dell'annotazione per {{camera}} è stato salvato nel file di configurazione. Riavvia Frigate per applicare le modifiche." + } + } + }, + "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..040d28680 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,8 +16,8 @@ "unknown": "Sconosciuto" }, "train": { - "title": "Addestra", - "aria": "Seleziona addestramento", + "title": "Riconoscimenti recenti", + "aria": "Seleziona i riconoscimenti recenti", "empty": "Non ci sono recenti tentativi di riconoscimento facciale" }, "button": { @@ -55,7 +55,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 +63,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 b264fecb7..774c151e8 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." @@ -147,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": { @@ -167,5 +170,16 @@ "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." + }, + "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/settings.json b/web/public/locales/it/views/settings.json index 48a1ca0bd..5f20486ff 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -10,7 +10,9 @@ "general": "Impostazioni generali - 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": { @@ -153,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": { @@ -236,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." }, "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", "point_one": "{{count}} punto", @@ -378,7 +381,10 @@ "users": "Utenti", "frigateplus": "Frigate+", "enrichments": "Miglioramenti", - "triggers": "Inneschi" + "triggers": "Inneschi", + "roles": "Ruoli", + "cameraManagement": "Gestione", + "cameraReview": "Rivedi" }, "users": { "dialog": { @@ -488,7 +494,11 @@ "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." + } }, "title": "Impostazioni generali", "storedLayouts": { @@ -739,7 +749,7 @@ "triggers": { "documentTitle": "Inneschi", "management": { - "title": "Gestione inneschi", + "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", @@ -760,7 +770,9 @@ }, "actions": { "alert": "Contrassegna come avviso", - "notification": "Invia notifica" + "notification": "Invia notifica", + "sub_label": "Aggiungi sottoetichetta", + "attribute": "Aggiungi attributo" }, "dialog": { "createTrigger": { @@ -778,25 +790,28 @@ "form": { "name": { "title": "Nome", - "placeholder": "Inserisci il nome dell'innesco", + "placeholder": "Assegna un nome a questo innesco", "error": { - "minLength": "Il nome deve essere lungo almeno 2 caratteri.", - "invalidCharacters": "Il nome può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", + "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" + "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 un'immagine", + "imagePlaceholder": "Seleziona una miniatura", "textPlaceholder": "Inserisci il contenuto del testo", - "imageDesc": "Seleziona un'immagine per attivare questa azione quando viene rilevata un'immagine simile.", + "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." @@ -807,14 +822,20 @@ "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. Scegli un'azione aggiuntiva da eseguire quando questo innesco si attiva.", + "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." } } }, @@ -829,6 +850,27 @@ "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": { @@ -850,7 +892,9 @@ "createRole": "Ruolo {{role}} creato con successo", "updateCameras": "Telecamere aggiornate per il ruolo {{role}}", "deleteRole": "Ruolo {{role}} eliminato con successo", - "userRolesUpdated": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere." + "userRolesUpdated_one": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Impossibile creare il ruolo: {{errorMessage}}", @@ -890,5 +934,231 @@ } } } + }, + "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": "Convalida e analisi finale prima di salvare la nuova telecamera. Connetti ogni flusso prima di salvare.", + "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." + }, + "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" + }, + "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": "Si prega di fornire un URL di flusso valido", + "testFailed": "Prova del flusso fallita: {{error}}" + }, + "step1": { + "description": "Inserisci i dettagli della tua telecamera e verifica la connessione.", + "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..." + } + }, + "step2": { + "description": "Configura i ruoli del flusso e aggiungi altri flussi per la tua telecamera.", + "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 del flusso riuscita!", + "testFailed": "Prova del flusso fallita", + "testFailedTitle": "Prova fallita", + "connected": "Connessa", + "notConnected": "Non connessa", + "featuresTitle": "Caratteristiche", + "go2rtc": "Riduci le connessioni alla telecamera", + "detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"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." + } + } + }, + "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/ja/common.json b/web/public/locales/ja/common.json index 90eeba7ef..ba84f3e2f 100644 --- a/web/public/locales/ja/common.json +++ b/web/public/locales/ja/common.json @@ -77,6 +77,14 @@ "length": { "feet": "フィート", "meters": "メートル" + }, + "data": { + "gbph": "GB/hour", + "gbps": "GB/s", + "kbph": "kB/hour", + "kbps": "kB/s", + "mbph": "MB/hour", + "mbps": "MB/s" } }, "label": { @@ -256,5 +264,8 @@ "title": "404", "desc": "ページが見つかりません" }, - "selectItem": "{{item}} を選択" + "selectItem": "{{item}} を選択", + "information": { + "pixels": "{{area}}ピクセル" + } } diff --git a/web/public/locales/ja/components/dialog.json b/web/public/locales/ja/components/dialog.json index c97fc475b..2c5f5e0d4 100644 --- a/web/public/locales/ja/components/dialog.json +++ b/web/public/locales/ja/components/dialog.json @@ -105,7 +105,8 @@ "button": { "export": "書き出し", "markAsReviewed": "レビュー済みにする", - "deleteNow": "今すぐ削除" + "deleteNow": "今すぐ削除", + "markAsUnreviewed": "未レビューに戻す" } }, "imagePicker": { diff --git a/web/public/locales/ja/views/classificationModel.json b/web/public/locales/ja/views/classificationModel.json new file mode 100644 index 000000000..54710f96c --- /dev/null +++ b/web/public/locales/ja/views/classificationModel.json @@ -0,0 +1,14 @@ +{ + "documentTitle": "分類モデル", + "button": { + "deleteImages": "画像を削除" + }, + "toast": { + "success": { + "deletedImage": "削除された画像", + "categorizedImage": "画像の分類に成功しました", + "trainedModel": "モデルを正常に学習させました。", + "trainingModel": "モデルのトレーニングを正常に開始しました。" + } + } +} diff --git a/web/public/locales/ja/views/explore.json b/web/public/locales/ja/views/explore.json index 5be336cfa..3e782f926 100644 --- a/web/public/locales/ja/views/explore.json +++ b/web/public/locales/ja/views/explore.json @@ -90,7 +90,7 @@ } }, "downloadingModels": { - "context": "Frigate はセマンティック検索をサポートするために必要な埋め込みモデルをダウンロードしています。ネットワーク速度により数分かかる場合があります。", + "context": "Frigate はセマンティック検索(意味理解型画像検索)をサポートするために必要な埋め込みモデルをダウンロードしています。ネットワーク速度により数分かかる場合があります。", "setup": { "visionModel": "ビジョンモデル", "visionModelFeatureExtractor": "ビジョンモデル特徴抽出器", diff --git a/web/public/locales/ja/views/faceLibrary.json b/web/public/locales/ja/views/faceLibrary.json index 7e7c2879d..f82b4e764 100644 --- a/web/public/locales/ja/views/faceLibrary.json +++ b/web/public/locales/ja/views/faceLibrary.json @@ -1,7 +1,7 @@ { "description": { "placeholder": "このコレクションの名前を入力", - "addFace": "顔データベースに新しいコレクションを追加する手順を案内します。", + "addFace": "最初の画像をアップロードして、フェイスライブラリに新しいコレクションを追加してください。", "invalidName": "無効な名前です。名前に使用できるのは英数字、スペース、アポストロフィ、アンダースコア、ハイフンのみです。" }, "details": { @@ -65,7 +65,7 @@ "selectImage": "画像ファイルを選択してください。" }, "dropActive": "ここに画像をドロップ…", - "dropInstructions": "画像をここにドラッグ&ドロップ、またはクリックして選択", + "dropInstructions": "画像をここにドラッグ&ドロップ、ペースト、またはクリックして選択", "maxSize": "最大サイズ: {{size}}MB" }, "nofaces": "顔はありません", diff --git a/web/public/locales/ja/views/live.json b/web/public/locales/ja/views/live.json index c47b6d817..cfcd5739d 100644 --- a/web/public/locales/ja/views/live.json +++ b/web/public/locales/ja/views/live.json @@ -91,7 +91,7 @@ }, "manualRecording": { "title": "オンデマンド録画", - "tips": "このカメラの録画保持設定に基づく手動イベントを開始します。", + "tips": "このカメラの録画保持設定に基づいて、即時スナップショットをダウンロードするか、手動イベントを開始してください。", "playInBackground": { "label": "バックグラウンドで再生", "desc": "プレーヤーが非表示の場合でもストリーミングを継続するにはこのオプションを有効にします。" @@ -136,6 +136,9 @@ "playInBackground": { "label": "バックグラウンドで再生", "tips": "プレーヤーが非表示でもストリーミングを継続するにはこのオプションを有効にします。" + }, + "debug": { + "picker": "デバッグモードではストリームの選択はできません。デバッグビューは常に 検出ロールに割り当てられたストリームを使用します。" } }, "cameraSettings": { @@ -165,5 +168,16 @@ "label": "カメラグループを編集" }, "exitEdit": "編集を終了" + }, + "noCameras": { + "title": "カメラが設定されていません", + "buttonText": "カメラを追加", + "description": "開始するには、カメラを接続してください。" + }, + "snapshot": { + "takeSnapshot": "即時スナップショットをダウンロード", + "noVideoSource": "スナップショットに使用できる映像ソースがありません。", + "captureFailed": "スナップショットの取得に失敗しました。", + "downloadStarted": "スナップショットのダウンロードを開始しました。" } } diff --git a/web/public/locales/ja/views/settings.json b/web/public/locales/ja/views/settings.json index 004fd73b3..000aac898 100644 --- a/web/public/locales/ja/views/settings.json +++ b/web/public/locales/ja/views/settings.json @@ -3,17 +3,19 @@ "authentication": "認証設定 - Frigate", "camera": "カメラ設定 - Frigate", "default": "設定 - Frigate", - "enrichments": "エンリッチメント設定 - Frigate", + "enrichments": "高度解析設定 - Frigate", "masksAndZones": "マスク/ゾーンエディタ - Frigate", "motionTuner": "モーションチューナー - Frigate", "object": "デバッグ - Frigate", "general": "一般設定 - Frigate", "frigatePlus": "Frigate+ 設定 - Frigate", - "notifications": "通知設定 - Frigate" + "notifications": "通知設定 - Frigate", + "cameraManagement": "カメラ設定 - Frigate", + "cameraReview": "カメラレビュー設定 - Frigate" }, "menu": { "ui": "UI", - "enrichments": "エンリッチメント", + "enrichments": "高度解析", "cameras": "カメラ設定", "masksAndZones": "マスク/ゾーン", "motionTuner": "モーションチューナー", @@ -21,7 +23,10 @@ "debug": "デバッグ", "users": "ユーザー", "notifications": "通知", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "cameraManagement": "管理", + "cameraReview": "レビュー", + "roles": "区分" }, "dialog": { "unsavedChanges": { @@ -84,8 +89,8 @@ } }, "enrichments": { - "title": "エンリッチメント設定", - "unsavedChanges": "未保存のエンリッチメント設定の変更", + "title": "高度解析設定", + "unsavedChanges": "未保存の高度解析設定の変更", "birdClassification": { "title": "鳥類分類", "desc": "量子化された TensorFlow モデルを使って既知の鳥を識別します。既知の鳥を認識した場合、その一般名を sub_label として追加します。この情報は UI、フィルタ、通知に含まれます。" @@ -136,9 +141,9 @@ "title": "ナンバープレート認識", "desc": "車両のナンバープレートを認識し、検出文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。" }, - "restart_required": "再起動が必要です(エンリッチメント設定を変更)", + "restart_required": "再起動が必要です(高度解析設定を変更)", "toast": { - "success": "エンリッチメント設定を保存しました。変更を適用するには Frigate を再起動してください。", + "success": "高度解析設定を保存しました。変更を適用するには Frigate を再起動してください。", "error": "設定変更の保存に失敗しました: {{errorMessage}}" } }, @@ -577,7 +582,7 @@ "createRole": "ロール {{role}} を作成しました", "updateCameras": "ロール {{role}} のカメラを更新しました", "deleteRole": "ロール {{role}} を削除しました", - "userRolesUpdated": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、すべてのカメラへの閲覧アクセスが付与されました。" + "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、すべてのカメラへの閲覧アクセスが付与されました。" }, "error": { "createRoleFailed": "ロールの作成に失敗しました: {{errorMessage}}", @@ -793,6 +798,11 @@ "error": { "min": "少なくとも1つのアクションを選択してください。" } + }, + "friendly_name": { + "title": "表示名", + "placeholder": "このトリガーの名前または説明", + "description": "このトリガーの表示名または説明文" } } }, @@ -807,6 +817,227 @@ "updateTriggerFailed": "トリガーの更新に失敗しました: {{errorMessage}}", "deleteTriggerFailed": "トリガーの削除に失敗しました: {{errorMessage}}" } + }, + "semanticSearch": { + "desc": "トリガーを使用するにはセマンティック検索を有効にする必要があります。", + "title": "セマンティック検索が無効です" + } + }, + "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 製カメラでは、追加のサブストリームが利用可能であり、カメラ本体の設定で有効化する必要があります。使用できる場合は、それらのストリームを確認して活用することを推奨します。" + } + } + }, + "title": "カメラを追加", + "description": "以下の手順に従って、Frigate に新しいカメラを追加します。", + "steps": { + "nameAndConnection": "名称と接続", + "streamConfiguration": "ストリーム設定", + "validationAndTesting": "検証とテスト" + }, + "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 を有効にし、カメラウィザードを再起動することを推奨します。" + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "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 のリストリーミングを使用してカメラへの接続数を削減します。" + } + } + }, + "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 66c77021b..da57fa7c3 100644 --- a/web/public/locales/ja/views/system.json +++ b/web/public/locales/ja/views/system.json @@ -3,7 +3,7 @@ "cameras": "カメラ統計 - Frigate", "general": "一般統計 - Frigate", "storage": "ストレージ統計 - Frigate", - "enrichments": "エンリッチメント統計 - Frigate", + "enrichments": "高度解析統計 - Frigate", "logs": { "frigate": "Frigate ログ - Frigate", "go2rtc": "Go2RTC ログ - Frigate", @@ -38,7 +38,7 @@ "general": { "title": "全般", "detector": { - "title": "ディテクタ", + "title": "検出器", "inferenceSpeed": "ディテクタ推論速度", "temperature": "ディテクタ温度", "cpuUsage": "ディテクタの CPU 使用率", @@ -167,7 +167,7 @@ "shmTooLow": "/dev/shm の割り当て({{total}} MB)は少なくとも {{min}} MB に増やす必要があります。" }, "enrichments": { - "title": "エンリッチメント", + "title": "高度解析", "infPerSecond": "毎秒推論回数", "embeddings": { "image_embedding": "画像埋め込み", diff --git a/web/public/locales/ko/audio.json b/web/public/locales/ko/audio.json index 3f0992b47..d9db04e9f 100644 --- a/web/public/locales/ko/audio.json +++ b/web/public/locales/ko/audio.json @@ -1,9 +1,9 @@ { "crying": "울음", "snoring": "코골이", - "singing": "노래하기", + "singing": "노래", "yell": "비명", - "speech": "발표", + "speech": "말소리", "babbling": "옹알이", "bicycle": "자전거", "a_capella": "아카펠라", @@ -11,5 +11,62 @@ "accordion": "아코디언", "acoustic_guitar": "어쿠스틱 기타", "car": "차량", - "motorcycle": "원동기" + "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 29eae6ae4..e5c8ef9a9 100644 --- a/web/public/locales/ko/common.json +++ b/web/public/locales/ko/common.json @@ -10,18 +10,262 @@ "30minutes": "30분", "5minutes": "5분", "untilRestart": "재시작 될 때까지", - "ago": "{{timeAgo}} 전" + "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" + "title": "404", + "documentTitle": "찾을 수 없음 - Frigate", + "desc": "페이지 찾을 수 없음" }, "accessDenied": { "title": "접근 거부", - "documentTitle": "접근 거부 - Frigate" + "documentTitle": "접근 거부 - Frigate", + "desc": "이 페이지 접근 권한이 없습니다." }, "menu": { "user": { - "account": "계정" + "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 d0b29a82a..65df51e36 100644 --- a/web/public/locales/ko/components/auth.json +++ b/web/public/locales/ko/components/auth.json @@ -1,11 +1,11 @@ { "form": { "user": "사용자명", - "password": "패스워드", + "password": "비밀번호", "login": "로그인", "errors": { "usernameRequired": "사용자명은 필수입니다", - "passwordRequired": "패스워드는 필수입니다", + "passwordRequired": "비밀번호는 필수입니다", "rateLimit": "너무 많이 시도했습니다. 다음에 다시 시도하세요.", "loginFailed": "로그인 실패", "unknownError": "알려지지 않은 에러. 로그를 확인하세요.", diff --git a/web/public/locales/ko/components/camera.json b/web/public/locales/ko/components/camera.json index a04bcaf34..67b1a2ee6 100644 --- a/web/public/locales/ko/components/camera.json +++ b/web/public/locales/ko/components/camera.json @@ -4,7 +4,83 @@ "add": "카메라 그룹 추가", "edit": "카메라 그룹 편집", "delete": { - "label": "카메라 그룹 삭제" + "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 9e562d24e..f701526ef 100644 --- a/web/public/locales/ko/components/dialog.json +++ b/web/public/locales/ko/components/dialog.json @@ -1,10 +1,92 @@ { "restart": { - "title": "Frigate를 정말로 다시 시작할까요?", + "title": "Frigate을 정말로 다시 시작할까요?", "button": "재시작", "restarting": { - "title": "Frigate가 재시작 중입니다", - "content": "이 페이지는 {{countdown}} 뒤에 새로 고침 됩니다." + "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 c0d378839..942b97c7d 100644 --- a/web/public/locales/ko/components/filter.json +++ b/web/public/locales/ko/components/filter.json @@ -1,3 +1,39 @@ { - "filter": "필터" + "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/input.json b/web/public/locales/ko/components/input.json index b697d11da..00a19b702 100644 --- a/web/public/locales/ko/components/input.json +++ b/web/public/locales/ko/components/input.json @@ -3,7 +3,7 @@ "downloadVideo": { "label": "비디오 다운로드", "toast": { - "success": "선택한 비디오들의 다운로드가 시작되었습니다." + "success": "다시보기 항목 다운로드가 시작되었습니다." } } } diff --git a/web/public/locales/ko/components/player.json b/web/public/locales/ko/components/player.json index 1d8cddaff..38ef7daac 100644 --- a/web/public/locales/ko/components/player.json +++ b/web/public/locales/ko/components/player.json @@ -1,7 +1,7 @@ { "submitFrigatePlus": { "submit": "제출", - "title": "이 프레임을 Frigate+로 전송하시겠습니까?" + "title": "이 프레임을 Frigate+에 제출하시겠습니까?" }, "stats": { "bandwidth": { @@ -10,17 +10,42 @@ }, "latency": { "short": { - "title": "지연" + "title": "지연", + "value": "{{seconds}} 초" }, - "title": "지연:" + "title": "지연:", + "value": "{{seconds}} 초" }, "streamType": { "short": "종류", "title": "스트림 종류:" }, - "totalFrames": "총 프레임:" + "totalFrames": "총 프레임:", + "droppedFrames": { + "title": "손실 프레임:", + "short": { + "title": "손실됨", + "value": "{{droppedFrames}} 프레임" + } + }, + "decodedFrames": "복원된 프레임:", + "droppedFrameRate": "프레임 손실률:" }, - "noRecordingsFoundForThisTime": "이 시간에는 녹화본이 없습니다", - "noPreviewFound": "미리 보기가 없습니다", - "noPreviewFoundFor": "{{cameraName}}에 미리보기가 없습니다" + "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 769a51276..e3506b15d 100644 --- a/web/public/locales/ko/objects.json +++ b/web/public/locales/ko/objects.json @@ -2,5 +2,119 @@ "person": "사람", "bicycle": "자전거", "car": "차량", - "motorcycle": "원동기" + "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 30311c9b7..bb8a84c2a 100644 --- a/web/public/locales/ko/views/configEditor.json +++ b/web/public/locales/ko/views/configEditor.json @@ -1,5 +1,18 @@ { "documentTitle": "설정 편집기 - Frigate", "configEditor": "설정 편집기", - "safeConfigEditor": "설정 편집기 (안전 모드)" + "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 c5b16d90f..971494a81 100644 --- a/web/public/locales/ko/views/events.json +++ b/web/public/locales/ko/views/events.json @@ -1,8 +1,51 @@ { - "alerts": "알림", - "detections": "탐지", + "alerts": "경보", + "detections": "대상 감지", "motion": { - "label": "움직임", - "only": "움직임만" - } + "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 17aee9232..231eade30 100644 --- a/web/public/locales/ko/views/explore.json +++ b/web/public/locales/ko/views/explore.json @@ -1,4 +1,31 @@ { "documentTitle": "탐색 - Frigate", - "generativeAI": "생성형 AI" + "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 adc9fc3b1..f4c902602 100644 --- a/web/public/locales/ko/views/exports.json +++ b/web/public/locales/ko/views/exports.json @@ -2,7 +2,7 @@ "documentTitle": "내보내기 - Frigate", "search": "검색", "noExports": "내보내기가 없습니다", - "deleteExport": "내보내기 제거", + "deleteExport": "내보내기 삭제", "deleteExport.desc": "{{exportName}}을 지우시겠습니까?", "editExport": { "title": "내보내기 이름 변경", diff --git a/web/public/locales/ko/views/faceLibrary.json b/web/public/locales/ko/views/faceLibrary.json index 09b5d1a2a..e1204d852 100644 --- a/web/public/locales/ko/views/faceLibrary.json +++ b/web/public/locales/ko/views/faceLibrary.json @@ -1,10 +1,84 @@ { "description": { "placeholder": "이 모음집의 이름을 입력해주세요", - "addFace": "얼굴 라이브러리에 새 컬렉션 추가하는 방법을 단계별로 알아보세요.", + "addFace": "얼굴 라이브러리에 새 모음집 추가하는 방법을 단계별로 알아보세요.", "invalidName": "잘못된 이름입니다. 이름은 문자, 숫자, 공백, 따옴표 ('), 밑줄 (_), 그리고 붙임표 (-)만 포함이 가능합니다." }, "details": { - "person": "사람" + "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 835a4a7f9..bfc44d18f 100644 --- a/web/public/locales/ko/views/live.json +++ b/web/public/locales/ko/views/live.json @@ -1,8 +1,183 @@ { - "documentTitle": "실시간 - Frigate", - "documentTitle.withCamera": "실시간 - {{camera}} - Frigate", + "documentTitle": "실시간 보기 - Frigate", + "documentTitle.withCamera": "{{camera}} - 실시간 보기 - Frigate", "lowBandwidthMode": "저대역폭 모드", "twoWayTalk": { - "enable": "양방향 말하기 활성화" + "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 aa2715665..2aa9934de 100644 --- a/web/public/locales/ko/views/recording.json +++ b/web/public/locales/ko/views/recording.json @@ -1,5 +1,12 @@ { "filter": "필터", "export": "내보내기", - "calendar": "달력" + "calendar": "날짜", + "filters": "필터", + "toast": { + "error": { + "noValidTimeSelected": "올바른 시간 범위를 선택하세요", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다" + } + } } diff --git a/web/public/locales/ko/views/settings.json b/web/public/locales/ko/views/settings.json index 90d864e5d..a5b1d5580 100644 --- a/web/public/locales/ko/views/settings.json +++ b/web/public/locales/ko/views/settings.json @@ -24,11 +24,99 @@ "documentTitle": { "default": "설정 - Frigate", "authentication": "인증 설정 - Frigate", - "camera": "카메라 설정 - 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 b6cae8635..4ed89d1ce 100644 --- a/web/public/locales/ko/views/system.json +++ b/web/public/locales/ko/views/system.json @@ -1,7 +1,186 @@ { "documentTitle": { "cameras": "카메라 통계 - Frigate", - "storage": "스토리지 통계 - Frigate", - "general": "기본 통계 - 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 a1b599904..2e8d481ce 100644 --- a/web/public/locales/lt/audio.json +++ b/web/public/locales/lt/audio.json @@ -366,7 +366,7 @@ "busy_signal": "Užimtas Signalas", "dial_tone": "Numerio Rinkimo Tonas", "telephone_dialing": "Telefono Rinkimas", - "ringtone": "Skambutis", + "ringtone": "Skambėjimo Tonas", "telephone_bell_ringing": "Skamba Telefonas", "alarm": "Signalizacija", "computer_keyboard": "Kopiuterio Klaviatūra", diff --git a/web/public/locales/lt/common.json b/web/public/locales/lt/common.json index 936cb217a..0930c68da 100644 --- a/web/public/locales/lt/common.json +++ b/web/public/locales/lt/common.json @@ -88,6 +88,14 @@ "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": { @@ -270,5 +278,8 @@ "desc": "Puslapis nerastas" }, "selectItem": "Pasirinkti {{item}}", - "readTheDocumentation": "Skaityti dokumentaciją" + "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/dialog.json b/web/public/locales/lt/components/dialog.json index 84b98af86..28069cb91 100644 --- a/web/public/locales/lt/components/dialog.json +++ b/web/public/locales/lt/components/dialog.json @@ -42,7 +42,7 @@ "label": "Rodyti transliacijos statistiką", "desc": "Įjungti šią galimybę rodyti transliacijos statistiką kaip pridėtinę informaciją kameros vaizde." }, - "debugView": "Derinimo Vaizdas" + "debugView": "Debug Vaizdas" }, "export": { "time": { @@ -83,7 +83,8 @@ "button": { "markAsReviewed": "Žymėti kaip peržiūrėtą", "export": "Eksportuoti", - "deleteNow": "Ištrinti Dabar" + "deleteNow": "Ištrinti Dabar", + "markAsUnreviewed": "Pažymėti kaip nematytą" }, "confirmDelete": { "desc": { diff --git a/web/public/locales/lt/views/classificationModel.json b/web/public/locales/lt/views/classificationModel.json new file mode 100644 index 000000000..f797f69d0 --- /dev/null +++ b/web/public/locales/lt/views/classificationModel.json @@ -0,0 +1,64 @@ +{ + "documentTitle": "Klasifikavimo Modeliai", + "button": { + "deleteClassificationAttempts": "Trinti Klasisifikavimo Nuotraukas", + "renameCategory": "Pervadinti Klasę", + "deleteCategory": "Trinti Klasę", + "deleteImages": "Trinti Nuotraukas", + "trainModel": "Treniruoti 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." + }, + "error": { + "deleteImageFailed": "Nepavyko ištrinti:{{errorMessage}}", + "deleteCategoryFailed": "Nepavyko ištrinti klasės:{{errorMessage}}", + "categorizeFailed": "Nepavyko kategorizuoti nuotraukos:{{errorMessage}}", + "trainingFailed": "Nepavyko pradėti modelio apmokymo:{{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į." + }, + "deleteDatasetImages": { + "title": "Ištrinti Imties Nuotraukas", + "desc": "Esate įsitikinę norite ištrinti {{count}} nautraukas iš {{dataset}}? Šis veiksmas negrįžtamas ir reikės iš naujo apmokinti modelį." + }, + "deleteTrainImages": { + "title": "Ištrinti Apmokymo Nuotraukas", + "desc": "Ar esate įsitikinę, kad norite ištrinti {{count}} nuotraukas? Šis veiksmas negrįžtamas." + }, + "renameCategory": { + "title": "Pervadinti Klasę", + "desc": "Įveskite naują vardą vietoje {{name}}. Jums reikės iš naujo apmokinti modelį, kad vardas į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" + }, + "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ų" + } + } +} diff --git a/web/public/locales/lt/views/events.json b/web/public/locales/lt/views/events.json index 79a222371..bd4ab2895 100644 --- a/web/public/locales/lt/views/events.json +++ b/web/public/locales/lt/views/events.json @@ -36,5 +36,17 @@ }, "detected": "aptikta", "suspiciousActivity": "Įtartina Veikla", - "threateningActivity": "Grėsminga Veikla" + "threateningActivity": "Grėsminga Veikla", + "detail": { + "noDataFound": "Peržiūrai informacijos nėra", + "aria": "Perjungti į detalų vaizdą", + "trackedObject_one": "objektas", + "trackedObject_other": "objektai", + "noObjectDetailData": "Nėra objekto detalių duomenų.", + "label": "Detalės" + }, + "objectTrack": { + "trackedPoint": "Susektas taškas", + "clickToSeek": "Spustelkite perkelti į šį laiką" + } } diff --git a/web/public/locales/lt/views/faceLibrary.json b/web/public/locales/lt/views/faceLibrary.json index b2cb8d332..721e119ce 100644 --- a/web/public/locales/lt/views/faceLibrary.json +++ b/web/public/locales/lt/views/faceLibrary.json @@ -1,8 +1,8 @@ { "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", @@ -45,7 +45,7 @@ } }, "createFaceLibrary": { - "nextSteps": "Kad sukurtumėte stiprų pagrindą:
  • Naudokite Apmokymų 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.
  • ", + "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ą" @@ -69,8 +69,8 @@ } }, "train": { - "title": "Traukinys", - "aria": "Pasirinkti traukinį", + "title": "Pastarieji Atpažinimai", + "aria": "Pasirinkti pastaruosius atpažinimus", "empty": "Pastaruoju metu nebuvo atliktas veidų atpažinimas" }, "selectFace": "Pasirinkti Veidą", @@ -91,7 +91,7 @@ "selectImage": "Prašome pasirinkti nuotraukos bylą." }, "dropActive": "Įkelkite nuotrauką čia…", - "dropInstructions": "Drag and drop nuotrauką čia, arba spragtelkite pasirinkti", + "dropInstructions": "Užvilkite nuotrauką čia, arba spragtelkite pasirinkti", "maxSize": "Max dydis: {{size}}MB" }, "nofaces": "Nėra veidų", diff --git a/web/public/locales/lt/views/live.json b/web/public/locales/lt/views/live.json index 5aa388aaf..06f1577f5 100644 --- a/web/public/locales/lt/views/live.json +++ b/web/public/locales/lt/views/live.json @@ -20,13 +20,13 @@ }, "cameraSettings": { "objectDetection": "Objektų Aptikimai", - "audioDetection": "Garso Aptikimai", + "audioDetection": "Garso Aptikimas", "title": "{{camera}} Nustatymai", "cameraEnabled": "Kamera įjungta", "recording": "Įrašinėjimas", "snapshots": "Momentinės Nuotraukos", - "transcription": "Garso Aprašymas", - "autotracking": "Autosekimas" + "transcription": "Garso Transkripcija", + "autotracking": "Automatinis sekimas" }, "ptz": { "move": { @@ -100,8 +100,8 @@ "disable": "Paslėpti Transliacijos Stats" }, "manualRecording": { - "title": "Įrašymai pagal poreikį", - "tips": "Kurti manualaus saugojimo nustatymus pagal įvykius iš šios kameros.", + "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ą." @@ -110,20 +110,20 @@ "label": "Rodytis Stats", "desc": "Įjungti šią funkciją, kad matytumėte transliacijos statistiką kameros vaizde." }, - "debugView": "Derinimo Vaizdas", - "start": "Pradėti įrašymą pagal poreikį", - "started": "Pradėtas rankinis pagal poreikį įrašinėjimas.", - "failedToStart": "Nepavyko pradėti rankinio įrašinėjimo pagal poreikį.", - "recordDisabledTips": "Tik momentinės nuotraukos bus saugomos, nes įrašinėjimai šiai kamera yra išjungti.", - "end": "Baigti įrašymą pagal poreikį", - "ended": "Baigtas rankinis įrašymas pagal poreikį.", - "failedToEnd": "Nepavyko nutraukti rankinio įrašinėjimo pagal poreikį." + "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": "Sustabdymo laikas: " + "forTime": "Sustabdyti laikui: " }, "stream": { "title": "Transliacija", @@ -131,8 +131,8 @@ "tips": { "title": "Šiai transliacijai garso išvestis turi būti sukonfiguruota naudojant go2rtc." }, - "available": "Šioje transliacijoje yra garsas", - "unavailable": "Šioje transliacijoje nėra garso" + "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.", @@ -146,6 +146,9 @@ "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": { @@ -162,8 +165,19 @@ "editLayout": { "label": "Redaguoti Išdėstymą", "group": { - "label": "Koreguoti Kamerų Grupę" + "label": "Redaguoti Kamerų Grupę" }, - "exitEdit": "Palikti Redagavimą" + "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/settings.json b/web/public/locales/lt/views/settings.json index bef8b8519..4fcd9cb8f 100644 --- a/web/public/locales/lt/views/settings.json +++ b/web/public/locales/lt/views/settings.json @@ -3,13 +3,15 @@ "default": "Nustatymai - Frigate", "authentication": "Autentifikavimo Nustatymai - Frigate", "camera": "Kameros Nustatymai - Frigate", - "object": "Derinti - Frigate", + "object": "Debug - Frigate", "general": "Bendrieji Nustatymai - Frigate", "frigatePlus": "Frigate+ Nustatymai - Frigate", "notifications": "Pranešimų Nustatymai - Frigate", "motionTuner": "Judesio Derinimas - Frigate", "enrichments": "Patobulinimų Nustatymai - Frigate", - "masksAndZones": "Maskavimo ir Zonų redaktorius - Frigate" + "masksAndZones": "Maskavimo ir Zonų redaktorius - Frigate", + "cameraManagement": "Valdyti Kameras - Frigate", + "cameraReview": "Kameros Peržiūros Nustatymai - Frigate" }, "menu": { "ui": "UI", @@ -17,11 +19,14 @@ "cameras": "Kameros Nustatymai", "masksAndZones": "Maskavimai / Zonos", "motionTuner": "Judesio Derintojas", - "debug": "Derinimas", + "debug": "Debug", "users": "Vartotojai", "notifications": "Pranešimai", "frigateplus": "Frigate+", - "triggers": "Trigeriai" + "triggers": "Trigeriai", + "roles": "Rolės", + "cameraManagement": "Valdymas", + "cameraReview": "Peržiūra" }, "dialog": { "unsavedChanges": { @@ -146,7 +151,7 @@ "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 derinimų.
    Pastaba: Tai neišjungs go2rtc sratų." + "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. ", @@ -229,7 +234,7 @@ "name": { "title": "Pavadinimas", "inputPlaceHolder": "Įveskite pavadinimą …", - "tips": "Pavadinimas privalo būti bent 2 simboliai ir negali būti toks pat kaip kita kamera ar zona." + "tips": "Pavadinimas privalo būti bent 2 simboliai, privalo turėti bent vieną raidę ir negali būti toks pat kaip kita kamera ar zona." }, "inertia": { "title": "Inercija", @@ -416,10 +421,10 @@ "currentRMS": "Dabartinis RMS", "currentdbFS": "Dabartinis dbFS" }, - "title": "Derinti", - "desc": "Derinimo vaizde rodomas tiesioginis vaizdas sekamų objektų ir statistikos. Objektų sąrašas rodo užvėlintą santrauką aptiktų objektų.", + "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": "Derinama", + "debugging": "Debugging", "objectList": "Objektų sąrašas", "noObjects": "Objektų nėra", "boundingBoxes": { @@ -618,6 +623,11 @@ "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." } } }, @@ -657,6 +667,10 @@ "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": { @@ -768,7 +782,9 @@ "toast": { "success": { "deleteRole": "Rolė {{role}} sėkmingai pašalinta", - "userRolesUpdated": "{{count}} šios rolės vartotojai buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "userRolesUpdated_one": "{{count}} šios rolės vartotojai buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "", "createRole": "Rolė {{role}} sėkmingai sukurta", "updateCameras": "Atnaujintos kameros rolei {{role}}" }, @@ -814,5 +830,48 @@ "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 testuokite prisijungimą.", + "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/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 af3d8b7b7..52058ab6e 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -241,10 +241,21 @@ "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" }, "toast": { "copyUrlToClipboard": "Nettadresse kopiert til utklippstavlen.", @@ -273,5 +284,17 @@ "desc": "Siden ble ikke funnet" }, "selectItem": "Velg {{item}}", - "readTheDocumentation": "Se dokumentasjonen" + "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/dialog.json b/web/public/locales/nb-NO/components/dialog.json index 58ff2e3f9..bebf2368c 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,7 +56,7 @@ } }, "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", @@ -117,14 +117,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-)merkelapp..." + "placeholder": "Søk etter (under-)etikett..." }, - "noImages": "Ingen miniatyrbilder funnet for dette kameraet" + "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 b7b81da9b..241102e08 100644 --- a/web/public/locales/nb-NO/components/filter.json +++ b/web/public/locales/nb-NO/components/filter.json @@ -1,14 +1,14 @@ { "filter": "Filter", "labels": { - "label": "Merkelapper", + "label": "Etiketter", "all": { "title": "Alle masker / soner", - "short": "Merkelapper" + "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", @@ -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" }, @@ -101,8 +101,8 @@ }, "timeRange": "Tidsrom", "subLabels": { - "label": "Under-Merkelapper", - "all": "Alle under-Merkelapper" + "label": "Underetiketter", + "all": "Alle underetiketter" }, "score": "Poengsum", "estimatedSpeed": "Estimert hastighet ({{unit}})", 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..0dc06ea32 --- /dev/null +++ b/web/public/locales/nb-NO/views/classificationModel.json @@ -0,0 +1,154 @@ +{ + "documentTitle": "Klassifiseringsmodeller", + "button": { + "deleteClassificationAttempts": "Slett klassifiseringsbilder", + "renameCategory": "Gi nytt navn til kategori", + "deleteCategory": "Slett kategori", + "deleteImages": "Slett bilder", + "trainModel": "Tren modell", + "addClassification": "Legg til klassifisering", + "deleteModels": "Slett modeller" + }, + "toast": { + "success": { + "deletedCategory": "Kategori slettet", + "deletedImage": "Bilder slettet", + "categorizedImage": "Bildet ble klassifisert", + "trainedModel": "Modellen ble trent.", + "trainingModel": "Modelltrening startet.", + "deletedModel_one": "{{count}} modell(er) ble slettet", + "deletedModel_other": "" + }, + "error": { + "deleteImageFailed": "Kunne ikke slette: {{errorMessage}}", + "deleteCategoryFailed": "Kunne ikke slette kategori: {{errorMessage}}", + "categorizeFailed": "Kunne ikke klassifisere bilde: {{errorMessage}}", + "trainingFailed": "Kunne ikke starte modelltrening: {{errorMessage}}", + "deleteModelFailed": "Kunne ikke slette modell: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Slett kategori", + "desc": "Er du sikker på at du vil slette kategorien {{name}}? Dette vil permanent slette alle tilknyttede bilder og kreve at modellen trenes på nytt." + }, + "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": "Gi nytt navn til kategori", + "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": "Nylig" + }, + "categories": "Kategorier", + "createCategory": { + "new": "Opprett ny kategori" + }, + "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 tilstands­klassifiseringsmodeller", + "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": "Kategorier", + "classesTip": "Lær om kategorier", + "classesStateDesc": "Definer de ulike tilstandene kamerasonen kan være i. For eksempel: 'åpen' og 'lukket' for en garasjeport.", + "classesObjectDesc": "Definer kategoriene du vil klassifisere oppdagede objekter i. For eksempel: 'bud', 'beboer', 'fremmed' for personklassifisering.", + "classPlaceholder": "Skriv inn kategorinavn...", + "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 kategori er påkrevd", + "classesUnique": "Kategorinavn må være unike", + "stateRequiresTwoClasses": "Tilstandsmodeller krever minst to kategorier", + "objectLabelRequired": "Velg en objektetikett", + "objectTypeRequired": "Velg en klassifiseringstype" + }, + "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 kategorien.", + "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" + } + }, + "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": "Poengsummen representerer gjennomsnittlig klassifiseringskonfidens på tvers av alle deteksjoner av dette objektet." + } +} diff --git a/web/public/locales/nb-NO/views/events.json b/web/public/locales/nb-NO/views/events.json index e333fb5ef..f28a06a0c 100644 --- a/web/public/locales/nb-NO/views/events.json +++ b/web/public/locales/nb-NO/views/events.json @@ -36,5 +36,24 @@ "selected_other": "{{count}} valgt", "detected": "detektert", "suspiciousActivity": "Mistenkelig aktivitet", - "threateningActivity": "Truende aktivitet" + "threateningActivity": "Truende aktivitet", + "detail": { + "noDataFound": "Ingen detaljer å inspisere", + "aria": "Vis/skjul detaljvisning", + "trackedObject_one": "objekt", + "trackedObject_other": "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" } diff --git a/web/public/locales/nb-NO/views/explore.json b/web/public/locales/nb-NO/views/explore.json index 9f14df89f..bfa70cde3 100644 --- a/web/public/locales/nb-NO/views/explore.json +++ b/web/public/locales/nb-NO/views/explore.json @@ -87,7 +87,7 @@ }, "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.", "audioTranscription": "Lydtranskripsjon ble forespurt." @@ -95,7 +95,7 @@ "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}}" } }, @@ -103,7 +103,7 @@ "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": { @@ -126,10 +126,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", @@ -142,9 +142,9 @@ "expandRegenerationMenu": "Utvid regenereringsmenyen", "regenerateFromSnapshot": "Regenerer fra øyeblikksbilde", "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" @@ -188,13 +188,23 @@ "audioTranscription": { "label": "Transkriber", "aria": "Forespør lydtranskripsjon" + }, + "showObjectDetails": { + "label": "Vis objektets sti" + }, + "hideObjectDetails": { + "label": "Gjem objektets sti" + }, + "viewTrackingDetails": { + "label": "Vis sporingsdetaljer", + "aria": "Vis sporingsdetaljene" } }, "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}}%" @@ -204,12 +214,13 @@ "details": "detaljer", "snapshot": "øyeblikksbilde", "video": "video", - "object_lifecycle": "objektets livssyklus" + "object_lifecycle": "objektets livssyklus", + "thumbnail": "miniatyrbilde" }, "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 øyeblikksbildet, 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", @@ -222,5 +233,53 @@ }, "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": "Forhold", + "area": "Område" + } + }, + "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. Start Frigate på nytt for å aktivere endringene." + } + } + }, + "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..49ec8520f 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", @@ -20,11 +20,11 @@ "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", + "aria": "Velg nylige gjenkjennelser", + "title": "Nylige gjenkjennelser", "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning" }, "selectFace": "Velg ansikt", @@ -57,7 +57,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 4c2711839..78270c0ce 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -62,7 +62,7 @@ "disable": "Deaktiver automatisk sporing" }, "manualRecording": { - "tips": "Start en manuell hendelse basert på kameraets innstillinger for opptaksbevaring.", + "tips": "Last ned et øyeblikksbilde, 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." @@ -71,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.", + "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 øyeblikksbilde bli lagret.", - "ended": "Avsluttet manuelt opptak på forespørsel.", - "failedToEnd": "Kunne ikke avslutte manuelt opptak på forespørsel." + "ended": "Avsluttet manuelt opptak.", + "failedToEnd": "Kunne ikke avslutte manuelt opptak." }, "audio": "Lyd", "suspend": { @@ -170,5 +170,16 @@ "transcription": { "enable": "Aktiver direkte lydtranskripsjon", "disable": "Deaktiver direkte lydtranskripsjon" + }, + "snapshot": { + "noVideoSource": "Ingen videokilde tilgjengelig for øyeblikksbilde.", + "captureFailed": "Kunne ikke ta øyeblikksbilde.", + "downloadStarted": "Nedlasting av øyeblikksbilde startet.", + "takeSnapshot": "Last ned øyeblikksbilde" + }, + "noCameras": { + "title": "Ingen kameraer konfigurert", + "description": "Kom i gang ved å koble et kamera til Frigate.", + "buttonText": "Legg til kamera" } } diff --git a/web/public/locales/nb-NO/views/search.json b/web/public/locales/nb-NO/views/search.json index baf25a900..4d81b38b0 100644 --- a/web/public/locales/nb-NO/views/search.json +++ b/web/public/locales/nb-NO/views/search.json @@ -12,14 +12,14 @@ "filter": { "label": { "cameras": "Kameraer", - "labels": "Merkelapper", + "labels": "Etiketter", "search_type": "Søketype", "after": "Etter", "min_score": "Min. poengsum", "max_score": "Maks. poengsum", "min_speed": "Min. hastighet", "zones": "Soner", - "sub_labels": "Under-merkelapper", + "sub_labels": "Underetiketter", "time_range": "Tidsintervall", "before": "Før", "max_speed": "Maks. hastighet", diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json index ba8d4c326..e37e546f1 100644 --- a/web/public/locales/nb-NO/views/settings.json +++ b/web/public/locales/nb-NO/views/settings.json @@ -10,7 +10,9 @@ "classification": "Klassifiseringsinnstillinger - Frigate", "frigatePlus": "Frigate+ innstillinger - Frigate", "notifications": "Meldingsvarsler Innstillinger - Frigate", - "enrichments": "Utvidelser Innstillinger - Frigate" + "enrichments": "Utvidelser Innstillinger - Frigate", + "cameraManagement": "Administrer kameraer - Frigate", + "cameraReview": "Innstillinger for kamerainspeksjon - Frigate" }, "menu": { "classification": "Klassifisering", @@ -23,7 +25,10 @@ "ui": "Brukergrensesnitt", "notifications": "Meldingsvarsler", "enrichments": "Utvidelser", - "triggers": "Utløsere" + "triggers": "Utløsere", + "cameraManagement": "Administrasjon", + "cameraReview": "Inspeksjon", + "roles": "Roller" }, "dialog": { "unsavedChanges": { @@ -45,6 +50,10 @@ "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." } }, "storedLayouts": { @@ -238,7 +247,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": { @@ -300,7 +310,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." }, "loiteringTime": { "title": "Oppholdstid", @@ -331,7 +341,7 @@ } }, "toast": { - "success": "Sone ({{zoneName}}) er lagret. Start Frigate på nytt for å bruke endringer." + "success": "Sone ({{zoneName}}) er lagret. Start Frigate på nytt for å aktivere endringer." } }, "motionMasks": { @@ -675,12 +685,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": { @@ -724,7 +734,7 @@ } }, "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", @@ -737,10 +747,10 @@ "triggers": { "documentTitle": "Utløsere", "management": { - "title": "Utløserhåndtering", + "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øs­er", + "addTrigger": "Legg til utløser", "table": { "name": "Navn", "type": "Type", @@ -758,7 +768,9 @@ }, "actions": { "alert": "Marker som varsel", - "notification": "Send meldingsvarsel" + "notification": "Send meldingsvarsel", + "sub_label": "Legg til underetikett", + "attribute": "Legg til attributt" }, "dialog": { "createTrigger": { @@ -776,25 +788,28 @@ "form": { "name": { "title": "Navn", - "placeholder": "Skriv inn navn på utløser", + "placeholder": "Navngi denne utløseren", "error": { - "minLength": "Navnet må være minst 2 tegn langt.", - "invalidCharacters": "Navnet kan bare inneholde bokstaver, tall, understreker og bindestreker.", + "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" + "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 bilde", + "imagePlaceholder": "Velg et miniatyrbilde", "textPlaceholder": "Skriv inn tekstinnhold", - "imageDesc": "Velg et bilde for å utløse denne handlingen når et lignende bilde oppdages.", + "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." @@ -805,14 +820,20 @@ "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. Velg en ekstra handling som skal utføres når denne utløseren aktiveres.", + "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" } } }, @@ -827,6 +848,27 @@ "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": { @@ -848,7 +890,8 @@ "createRole": "Rollen {{role}} ble opprettet", "updateCameras": "Kameraer oppdatert for rollen {{role}}", "deleteRole": "Rollen {{role}} ble slettet", - "userRolesUpdated": "{{count}} bruker(e) som var tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer." + "userRolesUpdated_one": "{{count}} bruker(e) som var tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer.", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Kunne ikke opprette rolle: {{errorMessage}}", @@ -888,5 +931,231 @@ } } } + }, + "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" + }, + "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 test tilkoblingen.", + "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..." + } + }, + "step2": { + "description": "Konfigurer strømroller og legg til flere strømmer for kameraet ditt.", + "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": "Strømmetesten var vellykket!", + "testFailed": "Strømmetesten feilet", + "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." + } + }, + "step3": { + "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": "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" + } + }, + "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": "Inspeksjon", + "desc": "Aktiver/deaktiver varsler og deteksjoner midlertidig for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke genereres nye inspeksjonselementer. ", + "alerts": "Varsler ", + "detections": "Deteksjoner " + }, + "reviewClassification": { + "title": "Inspeksjonssklassifisering", + "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 d242d69d8..3a642d804 100644 --- a/web/public/locales/nb-NO/views/system.json +++ b/web/public/locales/nb-NO/views/system.json @@ -118,7 +118,7 @@ "fetching": "Henter kameradata", "stream": "Strøm {{idx}}", "video": "Video:", - "fps": "Bilder per sekund:", + "fps": "FPS:", "unknown": "Ukjent", "tips": { "title": "Kamerainformasjon" 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 166dee4b5..5ff9ca549 100644 --- a/web/public/locales/nl/common.json +++ b/web/public/locales/nl/common.json @@ -128,10 +128,21 @@ "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" }, "menu": { "system": "Systeem", @@ -273,5 +284,17 @@ "documentTitle": "Niet gevonden - Frigate" }, "selectItem": "Selecteer {{item}}", - "readTheDocumentation": "Lees de documentatie" + "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/dialog.json b/web/public/locales/nl/components/dialog.json index 65ee012fd..0346d4dad 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,7 @@ "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." }, "fromTimeline": { "saveExport": "Export opslaan", @@ -105,9 +105,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": { @@ -121,10 +122,11 @@ } }, "imagePicker": { - "selectImage": "Kies thumbnail van gevolgd object", - "noImages": "Geen thumbnails gevonden voor deze camera", + "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/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..79956bf3d --- /dev/null +++ b/web/public/locales/nl/views/classificationModel.json @@ -0,0 +1,163 @@ +{ + "documentTitle": "Classificatiemodellen", + "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" + }, + "error": { + "deleteImageFailed": "Verwijderen mislukt: {{errorMessage}}", + "deleteCategoryFailed": "Het verwijderen van de klasse is mislukt: {{errorMessage}}", + "categorizeFailed": "Afbeelding categoriseren mislukt: {{errorMessage}}", + "trainingFailed": "Het starten van de modeltraining is mislukt: {{errorMessage}}", + "deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}", + "updateModelFailed": "Bijwerken van model mislukt: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Klasse verwijderen", + "desc": "Weet je zeker dat je de klasse {{name}} wilt verwijderen? Hiermee worden alle bijbehorende afbeeldingen permanent verwijderd en moet het model opnieuw worden getraind." + }, + "deleteDatasetImages": { + "title": "Datasetafbeeldingen verwijderen", + "desc": "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": "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 State-classificatiemodellen beschikbaar", + "description": "Maak een aangepast model om statuswijzigingen in specifieke cameragebieden te monitoren en te classificeren.", + "buttonText": "Maak een toestandsmodel" + } + }, + "wizard": { + "title": "Nieuwe classificatie maken", + "steps": { + "nameAndDefine": "Naam & definiëren", + "stateArea": "Staatsgebied", + "chooseExamples": "Voorbeelden kiezen" + }, + "step1": { + "description": "Toestandsmodellen 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 toestanden waarin het cameragebied zich kan bevinden. Bijvoorbeeld: ‘open’ en ‘dicht’ 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": "Toestandsmodellen vereisen minimaal 2 klassen", + "objectLabelRequired": "Selecteer een objectlabel", + "objectTypeRequired": "Selecteer een classificatietype" + }, + "states": "Staten" + }, + "step2": { + "description": "Selecteer camera’s en definieer voor elke camera het te monitoren gebied. Het model zal de toestand 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" + } + }, + "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": "Weet u zeker dat u {{count}} model(len) 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." + }, + "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." + } +} diff --git a/web/public/locales/nl/views/events.json b/web/public/locales/nl/views/events.json index 3f42b6c29..643fef8b0 100644 --- a/web/public/locales/nl/views/events.json +++ b/web/public/locales/nl/views/events.json @@ -36,5 +36,24 @@ "selected_one": "{{count}} geselecteerd", "detected": "gedetecteerd", "suspiciousActivity": "Verdachte activiteit", - "threateningActivity": "Bedreigende activiteit" + "threateningActivity": "Bedreigende activiteit", + "detail": { + "noDataFound": "Geen gedetailleerde gegevens om te beoordelen", + "aria": "Detailweergave in- of uitschakelen", + "trackedObject_one": "object", + "trackedObject_other": "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" } diff --git a/web/public/locales/nl/views/explore.json b/web/public/locales/nl/views/explore.json index a7a8f41e4..10fa78697 100644 --- a/web/public/locales/nl/views/explore.json +++ b/web/public/locales/nl/views/explore.json @@ -33,7 +33,8 @@ "details": "Details", "video": "video", "snapshot": "snapshot", - "object_lifecycle": "objectlevenscyclus" + "object_lifecycle": "objectlevenscyclus", + "thumbnail": "thumbnail" }, "objectLifecycle": { "createObjectMask": "Objectmasker maken", @@ -154,7 +155,7 @@ }, "recognizedLicensePlate": "Erkende kentekenplaat", "snapshotScore": { - "label": "Snapshot scoren" + "label": "Snapshot score" }, "score": { "label": "Score" @@ -195,6 +196,16 @@ "audioTranscription": { "label": "Transcriberen", "aria": "Audiotranscriptie aanvragen" + }, + "showObjectDetails": { + "label": "Objectpad weergeven" + }, + "hideObjectDetails": { + "label": "Verberg objectpad" + }, + "viewTrackingDetails": { + "label": "Bekijk trackinggegevens", + "aria": "Toon de trackinggegevens" } }, "noTrackedObjects": "Geen gevolgde objecten gevonden", @@ -212,7 +223,7 @@ "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}}", @@ -222,5 +233,53 @@ }, "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" + } + }, + "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": "TIP: Stel je voor dat er een clip is waarin een persoon van links naar rechts loopt. Als het objectkader in de tijdlijn van de activiteit steeds links van de persoon ligt, dan moet de waarde verlaagd worden. Op dezelfde manier als het objectkader consequent vóór de persoon ligt dus vooruitloopt, moet de waarde verhoogd worden.", + "toast": { + "success": "Annotatieverschuiving voor {{camera}} is opgeslagen in het configuratiebestand. Herstart Frigate om je wijzigingen toe te passen." + } + } + }, + "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..11b8fbd27 100644 --- a/web/public/locales/nl/views/faceLibrary.json +++ b/web/public/locales/nl/views/faceLibrary.json @@ -13,12 +13,12 @@ "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", + "title": "Recente herkenningen", + "aria": "Selecteer recente herkenningen", "empty": "Er zijn geen recente pogingen tot gezichtsherkenning" }, "selectFace": "Selecteer gezicht", @@ -46,7 +46,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 +56,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 25b6b3e6a..798d24368 100644 --- a/web/public/locales/nl/views/live.json +++ b/web/public/locales/nl/views/live.json @@ -91,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", @@ -128,7 +128,7 @@ "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": { @@ -170,5 +170,16 @@ "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" } } diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json index e60e3af39..d62df1215 100644 --- a/web/public/locales/nl/views/settings.json +++ b/web/public/locales/nl/views/settings.json @@ -10,7 +10,9 @@ "general": "Algemene 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", @@ -23,7 +25,10 @@ "cameras": "Camera-instellingen", "frigateplus": "Frigate+", "enrichments": "Verrijkingen", - "triggers": "Triggers" + "triggers": "Triggers", + "roles": "Functie", + "cameraManagement": "Beheer", + "cameraReview": "Beoordeel" }, "dialog": { "unsavedChanges": { @@ -45,6 +50,10 @@ "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." } }, "title": "Algemene instellingen", @@ -238,7 +247,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": { @@ -292,7 +302,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." }, "inertia": { "title": "Traagheid", @@ -737,7 +747,7 @@ "triggers": { "documentTitle": "Triggers", "management": { - "title": "Triggerbeheer", + "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", @@ -758,7 +768,9 @@ }, "actions": { "alert": "Markeren als waarschuwing", - "notification": "Melding verzenden" + "notification": "Melding verzenden", + "sub_label": "Sublabel toevoegen", + "attribute": "Attribuut toevoegen" }, "dialog": { "createTrigger": { @@ -776,25 +788,28 @@ "form": { "name": { "title": "Naam", - "placeholder": "Voer de naam van de trigger in", + "placeholder": "Geef deze trigger een naam", "error": { - "minLength": "De naam moet minimaal 2 tekens lang zijn.", - "invalidCharacters": "De naam mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.", + "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" + "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 afbeelding", + "imagePlaceholder": "Selecteer een thumbnail", "textPlaceholder": "Tekst invoeren", - "imageDesc": "Selecteer een afbeelding om deze actie te activeren wanneer een vergelijkbare afbeelding wordt gedetecteerd.", + "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." @@ -805,14 +820,20 @@ "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 verstuurt Frigate een MQTT-bericht voor alle triggers. Kies een extra actie die moet worden uitgevoerd wanneer deze trigger wordt geactiveerd.", + "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." } } }, @@ -827,6 +848,27 @@ "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": { @@ -848,7 +890,8 @@ "createRole": "Rol {{role}} succesvol aangemaakt", "updateCameras": "Camera's bijgewerkt voor rol {{role}}", "deleteRole": "Rol {{role}} succesvol verwijderd", - "userRolesUpdated": "{{count}} gebruiker(s) die aan deze rol waren toegewezen, zijn bijgewerkt naar ‘kijker’, die toegang heeft tot alle camera’s." + "userRolesUpdated_one": "{{count}} gebruiker(s) die aan deze rol waren toegewezen, zijn bijgewerkt naar ‘kijker’, die toegang heeft tot alle camera’s.", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Kan rol niet aanmaken: {{errorMessage}}", @@ -888,5 +931,231 @@ } } } + }, + "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" + }, + "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 je cameragegevens in en test de verbinding.", + "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..." + } + }, + "step2": { + "description": "Configureer streamrollen en voeg extra streams toe voor uw camera.", + "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": "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": "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." + } + }, + "step3": { + "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": "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." + } + }, + "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/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/common.json b/web/public/locales/pl/common.json index 058c74733..0ac1208b0 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -280,5 +280,8 @@ "title": "Zapisz" } }, - "readTheDocumentation": "Przeczytaj dokumentację" + "readTheDocumentation": "Przeczytaj dokumentację", + "information": { + "pixels": "{{area}}px" + } } diff --git a/web/public/locales/pl/components/dialog.json b/web/public/locales/pl/components/dialog.json index 2066a667d..73a29704b 100644 --- a/web/public/locales/pl/components/dialog.json +++ b/web/public/locales/pl/components/dialog.json @@ -81,7 +81,8 @@ "button": { "markAsReviewed": "Oznacz jako sprawdzone", "deleteNow": "Usuń teraz", - "export": "Eksportuj" + "export": "Eksportuj", + "markAsUnreviewed": "Oznacz jako niesprawdzone" }, "confirmDelete": { "title": "Potwierdź Usunięcie", diff --git a/web/public/locales/pl/views/classificationModel.json b/web/public/locales/pl/views/classificationModel.json new file mode 100644 index 000000000..faf2aa416 --- /dev/null +++ b/web/public/locales/pl/views/classificationModel.json @@ -0,0 +1,7 @@ +{ + "documentTitle": "Modele klasyfikacji", + "button": { + "deleteClassificationAttempts": "Usuń obrazy klasyfikacyjne", + "renameCategory": "Zmień nazwę klasy" + } +} diff --git a/web/public/locales/pl/views/faceLibrary.json b/web/public/locales/pl/views/faceLibrary.json index a10105faa..be17253a0 100644 --- a/web/public/locales/pl/views/faceLibrary.json +++ b/web/public/locales/pl/views/faceLibrary.json @@ -1,7 +1,7 @@ { "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." }, diff --git a/web/public/locales/pl/views/live.json b/web/public/locales/pl/views/live.json index e2ca8d9bd..805e49efa 100644 --- a/web/public/locales/pl/views/live.json +++ b/web/public/locales/pl/views/live.json @@ -113,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": { @@ -167,5 +170,10 @@ "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.", + "title": "Nie ustawiono żadnej kamery" } } diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json index d2852b7ec..0291f39c5 100644 --- a/web/public/locales/pl/views/settings.json +++ b/web/public/locales/pl/views/settings.json @@ -10,7 +10,8 @@ "motionTuner": "Konfigurator Ruchu", "debug": "Debugowanie", "enrichments": "Wzbogacenia", - "triggers": "Wyzwalacze" + "triggers": "Wyzwalacze", + "roles": "Role" }, "dialog": { "unsavedChanges": { @@ -83,7 +84,8 @@ "motionTuner": "Konfigurator Ruchu - Frigate", "object": "Debug - Frigate", "notifications": "Ustawienia powiadomień - Frigate", - "enrichments": "Ustawienia wzbogacania - Frigate" + "enrichments": "Ustawienia wzbogacania - Frigate", + "cameraManagement": "Zarządzanie kamerami – Frigate" }, "classification": { "title": "Ustawienia Klasyfikacji", @@ -755,7 +757,9 @@ "createRole": "Utworzono rolę {{role}}", "updateCameras": "Zaktualizowano kamery dla roli {{role}}", "deleteRole": "Rola {{role}} została usunięta", - "userRolesUpdated": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer." + "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}}", @@ -875,6 +879,11 @@ "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." } } }, @@ -889,6 +898,10 @@ "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." } } } diff --git a/web/public/locales/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json index 74140c039..b36f09902 100644 --- a/web/public/locales/pt-BR/audio.json +++ b/web/public/locales/pt-BR/audio.json @@ -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 69e50231c..e1ab1e525 100644 --- a/web/public/locales/pt-BR/common.json +++ b/web/public/locales/pt-BR/common.json @@ -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": { @@ -270,5 +278,8 @@ "title": "404", "desc": "Página não encontrada" }, - "readTheDocumentation": "Leia a documentação" + "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/dialog.json b/web/public/locales/pt-BR/components/dialog.json index 2e0670622..6f15f9855 100644 --- a/web/public/locales/pt-BR/components/dialog.json +++ b/web/public/locales/pt-BR/components/dialog.json @@ -108,7 +108,8 @@ "button": { "markAsReviewed": "Marcar como revisado", "export": "Exportar", - "deleteNow": "Deletar Agora" + "deleteNow": "Deletar Agora", + "markAsUnreviewed": "Marcar como não revisado" } }, "imagePicker": { 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..5cfed4b10 --- /dev/null +++ b/web/public/locales/pt-BR/views/classificationModel.json @@ -0,0 +1,39 @@ +{ + "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" + }, + "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": "Modelo(s) {{count}} excluído(s) com sucesso", + "deletedModel_many": "", + "deletedModel_other": "" + }, + "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}}" + } + }, + "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." + }, + "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." + } +} diff --git a/web/public/locales/pt-BR/views/events.json b/web/public/locales/pt-BR/views/events.json index 2e7eac4cb..8edaa67ca 100644 --- a/web/public/locales/pt-BR/views/events.json +++ b/web/public/locales/pt-BR/views/events.json @@ -36,5 +36,23 @@ "camera": "Câmera", "detected": "detectado", "suspiciousActivity": "Atividade Suspeita", - "threateningActivity": "Atividade de Ameaça" + "threateningActivity": "Atividade de Ameaça", + "detail": { + "noDataFound": "Nenhum dado de detalhe para revisar", + "aria": "Alternar visualização de detalhe", + "trackedObject_one": "objeto", + "trackedObject_other": "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 9de511f9d..bb3e6fdab 100644 --- a/web/public/locales/pt-BR/views/explore.json +++ b/web/public/locales/pt-BR/views/explore.json @@ -111,7 +111,8 @@ "details": "detalhes", "snapshot": "captura de imagem", "video": "vídeo", - "object_lifecycle": "ciclo de vida do objeto" + "object_lifecycle": "ciclo de vida do objeto", + "thumbnail": "thumbnail" }, "objectLifecycle": { "title": "Ciclo de Vida do Objeto", 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 912236cf4..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,7 +33,7 @@ "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", @@ -45,7 +45,7 @@ "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 8ac0b6188..ef6a2516b 100644 --- a/web/public/locales/pt-BR/views/live.json +++ b/web/public/locales/pt-BR/views/live.json @@ -86,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." @@ -170,5 +170,16 @@ "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 9f755ab2d..dbf8cf433 100644 --- a/web/public/locales/pt-BR/views/settings.json +++ b/web/public/locales/pt-BR/views/settings.json @@ -9,7 +9,9 @@ "object": "Debug - Frigate", "general": "Configurações Gerais - 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", @@ -21,7 +23,10 @@ "motionTuner": "Ajuste de Movimento", "debug": "Depurar", "enrichments": "Enriquecimentos", - "triggers": "Gatilhos" + "triggers": "Gatilhos", + "roles": "Papéis", + "cameraManagement": "Gerenciamento", + "cameraReview": "Revisar" }, "dialog": { "unsavedChanges": { @@ -751,6 +756,11 @@ "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." } } }, @@ -765,6 +775,10 @@ "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": { @@ -786,7 +800,9 @@ "createRole": "Papel {{role}} criado com sucesso", "updateCameras": "Câmeras atualizados para o papel {{role}}", "deleteRole": "Papel {{role}} apagado com sucesso", - "userRolesUpdated": "{{count}} usuário(os) atribuídos a esse papel foram atualizados para 'visualizador', que possui acesso a todas as câmeras." + "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}}", @@ -826,5 +842,62 @@ } } } + }, + "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/views/classificationModel.json b/web/public/locales/pt/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/pt/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/pt/views/settings.json b/web/public/locales/pt/views/settings.json index 8b4f92cf5..1bab92d78 100644 --- a/web/public/locales/pt/views/settings.json +++ b/web/public/locales/pt/views/settings.json @@ -10,7 +10,9 @@ "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", @@ -23,7 +25,10 @@ "notifications": "Notificações", "frigateplus": "Frigate+", "enrichments": "Avançado", - "triggers": "Gatilhos" + "triggers": "Gatilhos", + "cameraManagement": "Gestão", + "cameraReview": "Rever", + "roles": "Papéis" }, "dialog": { "unsavedChanges": { @@ -815,6 +820,11 @@ "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." } } }, @@ -829,6 +839,10 @@ "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": { @@ -850,7 +864,9 @@ "createRole": "Papel {{role}} criado com sucesso", "updateCameras": "Câmaras atualizados para o papel {{role}}", "deleteRole": "Papel {{role}} apagado com sucesso", - "userRolesUpdated": "{{count}} utilizador(os) atribuídos a este papel foram atualizados para 'visualizador', que possui acesso a todas as câmaras." + "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}}", @@ -890,5 +906,81 @@ } } } + }, + "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/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 1e36296f6..d232a8047 100644 --- a/web/public/locales/ro/common.json +++ b/web/public/locales/ro/common.json @@ -226,10 +226,21 @@ "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" }, "selectItem": "Selectează {{item}}", "pagination": { @@ -270,5 +281,17 @@ "title": "404", "desc": "Pagină negăsită" }, - "readTheDocumentation": "Citește documentația" + "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/dialog.json b/web/public/locales/ro/components/dialog.json index 626c30727..c3a451368 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,7 +83,7 @@ "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", @@ -105,7 +106,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" }, @@ -128,6 +129,7 @@ "search": { "placeholder": "Caută după etichetă sau subetichetă..." }, - "noImages": "Nu s-au găsit miniaturi pentru această cameră" + "noImages": "Nu s-au găsit miniaturi pentru această cameră", + "unknownLabel": "Imaginea declanșator salvată" } } diff --git a/web/public/locales/ro/views/classificationModel.json b/web/public/locales/ro/views/classificationModel.json new file mode 100644 index 000000000..1e48893ba --- /dev/null +++ b/web/public/locales/ro/views/classificationModel.json @@ -0,0 +1,164 @@ +{ + "documentTitle": "Modele de clasificare", + "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" + }, + "error": { + "deleteImageFailed": "Ștergerea a eșuat: {{errorMessage}}", + "deleteCategoryFailed": "Ștergerea clasei a eșuat: {{errorMessage}}", + "categorizeFailed": "Categorisirea imaginii a eșuat: {{errorMessage}}", + "trainingFailed": "Pornirea antrenamentului modelului a eșuat: {{errorMessage}}", + "deleteModelFailed": "Ștergerea modelului a eșuat: {{errorMessage}}", + "updateModelFailed": "Actualizarea modelului a eșuat: {{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." + }, + "deleteDatasetImages": { + "title": "Șterge imaginile setului de date", + "desc": "Sigur doriți să ștergeți {{count}} imagini din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului." + }, + "deleteTrainImages": { + "title": "Șterge imaginile de antrenament", + "desc": "Sigur doriți să ștergeți {{count}} 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": "Recente", + "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" + }, + "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" + } + }, + "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": "Sigur doriți să ștergeți {{count}} model(e)? 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." + }, + "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." + } +} diff --git a/web/public/locales/ro/views/events.json b/web/public/locales/ro/views/events.json index f250d6feb..c0210ce36 100644 --- a/web/public/locales/ro/views/events.json +++ b/web/public/locales/ro/views/events.json @@ -36,5 +36,24 @@ "selected_one": "{{count}} selectate", "selected_other": "{{count}} selectate", "suspiciousActivity": "Activitate suspectă", - "threateningActivity": "Activitate amenințătoare" + "threateningActivity": "Activitate amenințătoare", + "detail": { + "noDataFound": "Nicio dată detaliată de revizuit", + "aria": "Comută vizualizarea detaliată", + "trackedObject_one": "obiect", + "trackedObject_other": "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ă" } diff --git a/web/public/locales/ro/views/explore.json b/web/public/locales/ro/views/explore.json index 66e3abc29..2c9e1c072 100644 --- a/web/public/locales/ro/views/explore.json +++ b/web/public/locales/ro/views/explore.json @@ -33,7 +33,8 @@ "details": "detalii", "snapshot": "snapshot", "video": "video", - "object_lifecycle": "ciclul de viață al obiectului" + "object_lifecycle": "ciclul de viață al obiectului", + "thumbnail": "miniatură" }, "objectLifecycle": { "lifecycleItemDesc": { @@ -70,7 +71,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": { @@ -200,12 +201,22 @@ "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" } }, "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", @@ -224,5 +235,53 @@ }, "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" + } + }, + "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 fluxul de detectare al camerei tale, dar sunt suprapuse pe imaginile din fluxul de înregistrare. Este puțin probabil ca cele două fluxuri 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": "SFAT: Imaginează-ți că există un clip al unui eveniment cu o persoană care merge de la stânga la dreapta. Dacă caseta delimitatoare a cronologiei evenimentului este constant în stânga persoanei, atunci valoarea ar trebui să fie scăzută. În mod similar, dacă o persoană merge de la stânga la dreapta și caseta delimitatoare este constant în fața persoanei, atunci valoarea ar trebui să fie crescută.", + "toast": { + "success": "Decalajul de adnotare pentru {{camera}} a fost salvat în fișierul de configurare. Repornește Frigate pentru a aplica modificările." + } + } + }, + "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..a1e03f734 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,15 @@ "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" }, "steps": { "description": { @@ -88,7 +88,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 a998c49a8..ed57d49f7 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,7 +36,7 @@ }, "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", @@ -86,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." @@ -100,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." @@ -134,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": { @@ -167,5 +170,16 @@ "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ă" } } diff --git a/web/public/locales/ro/views/search.json b/web/public/locales/ro/views/search.json index 9e80fdc3b..94d035a5e 100644 --- a/web/public/locales/ro/views/search.json +++ b/web/public/locales/ro/views/search.json @@ -33,7 +33,7 @@ "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 2dcb1a4ed..262d50547 100644 --- a/web/public/locales/ro/views/settings.json +++ b/web/public/locales/ro/views/settings.json @@ -10,7 +10,9 @@ "object": "Depanare - Frigate", "general": "Setări generale - 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", @@ -22,7 +24,10 @@ "users": "Utilizatori", "notifications": "Notificări", "frigateplus": "Frigate+", - "triggers": "Declanșatoare" + "triggers": "Declanșatoare", + "roles": "Roluri", + "cameraManagement": "Administrare", + "cameraReview": "Revizuire" }, "dialog": { "unsavedChanges": { @@ -45,6 +50,10 @@ "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." } }, "storedLayouts": { @@ -245,7 +254,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, trebuie să conțină cel puțin o literă și nu trebuie să fie identic cu numele unei camere sau al unei alte zone existente." }, "inertia": { "title": "Inerție", @@ -262,7 +271,7 @@ "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." }, @@ -277,7 +286,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": { @@ -325,7 +334,7 @@ "title": "{{polygonName}} a fost salvat. Repornește Frigate pentru a aplica modificările." } }, - "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)", @@ -349,7 +358,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": { @@ -676,7 +686,7 @@ "triggers": { "documentTitle": "Declanșatoare", "management": { - "title": "Gestionarea declanșatoarelor", + "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", @@ -697,7 +707,9 @@ }, "actions": { "alert": "Marchează ca alertă", - "notification": "Trimite notificare" + "notification": "Trimite notificare", + "sub_label": "Adaugă subeticheta", + "attribute": "Adaugă atribut" }, "dialog": { "createTrigger": { @@ -715,25 +727,28 @@ "form": { "name": { "title": "Nume", - "placeholder": "Introdu numele declanșatorului", + "placeholder": "Denumește acest declanșator", "error": { - "minLength": "Numele trebuie să aibă cel puțin 2 caractere.", - "invalidCharacters": "Numele poate conține doar litere, cifre, underscore-uri și cratime.", + "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" + "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 imagine", + "imagePlaceholder": "Selectează o miniatură", "textPlaceholder": "Introdu conținutul textului", - "imageDesc": "Selectează o imagine pentru a declanșa această acțiune atunci când o imagine similară este detectată.", + "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." @@ -744,14 +759,20 @@ "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": "Implicit, Frigate trimite un mesaj MQTT pentru toate declanșatoarele. Alegeți o acțiune suplimentară de efectuat atunci când acest declanșator se activează.", + "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." } } }, @@ -766,6 +787,27 @@ "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": { @@ -787,7 +829,9 @@ "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": "{{count}} utilizator(i) atribuiți acestui rol au fost actualizați la „vizualizator”, care are acces la toate camerele." + "userRolesUpdated_one": "{{count}} utilizator(i) atribuiți acestui rol au fost actualizați la „vizualizator”, care are acces la toate camerele.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Crearea rolului a eșuat: {{errorMessage}}", @@ -827,5 +871,231 @@ } } } + }, + "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" + }, + "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": "Introdu detaliile camerei și testează conexiunea.", + "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ă..." + } + }, + "step2": { + "description": "Configurează rolurile de streaming și adaugă stream-uri suplimentare pentru camera ta.", + "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 streaming a reușit!", + "testFailed": "Testul de streaming a eșuat", + "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ă." + } + }, + "step3": { + "description": "Validare finală și analiză înainte de a salva noua cameră. Conectează fiecare stream înainte de a salva.", + "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." + } + }, + "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 057370448..ee79d9780 100644 --- a/web/public/locales/ro/views/system.json +++ b/web/public/locales/ro/views/system.json @@ -121,7 +121,7 @@ "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" }, diff --git a/web/public/locales/ru/common.json b/web/public/locales/ru/common.json index ee4a0df10..8b15aed2a 100644 --- a/web/public/locales/ru/common.json +++ b/web/public/locales/ru/common.json @@ -89,7 +89,7 @@ "24hour": "d MMM, yyyy" } }, - "selectItem": "Выбор {{item}}", + "selectItem": "Выбрать {{item}}", "button": { "apply": "Применить", "done": "Готово", @@ -138,6 +138,14 @@ "length": { "meters": "метры", "feet": "футы" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/час", + "mbph": "МБ/час", + "gbph": "ГБ/час" } }, "menu": { @@ -280,6 +288,8 @@ "viewer": "Наблюдатель", "desc": "Администраторы имеют полный доступ ко всем функциям в интерфейсе Frigate. Наблюдатели ограничены просмотром камер, элементов просмотра и архивных записей." }, - "selectItem": "Выбрать {{item}}", - "readTheDocumentation": "Читать документацию" + "readTheDocumentation": "Читать документацию", + "information": { + "pixels": "{{area}}px" + } } 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/views/classificationModel.json b/web/public/locales/ru/views/classificationModel.json new file mode 100644 index 000000000..cabb7793e --- /dev/null +++ b/web/public/locales/ru/views/classificationModel.json @@ -0,0 +1,40 @@ +{ + "documentTitle": "Модели классификации", + "details": { + "scoreInfo": "Оценка представляет собой среднюю степень достоверности классификации по всем обнаружениям данного объекта." + }, + "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": "Успешно обновлена конфигурация модели" + }, + "error": { + "deleteImageFailed": "Не удалось удалить: {{errorMessage}}", + "deleteCategoryFailed": "Не удалось удалить класс: {{errorMessage}}", + "deleteModelFailed": "Не удалось удалить модель: {{errorMessage}}", + "categorizeFailed": "Не удалось классифицировать изображение: {{errorMessage}}", + "trainingFailed": "Не удалось начать обучение модели: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Удалить класс", + "desc": "Вы уверены, что хотите удалить класс {{name}}? Это приведёт к безвозвратному удалению всех связанных с ним изображений и потребует повторного обучения модели." + } +} diff --git a/web/public/locales/ru/views/events.json b/web/public/locales/ru/views/events.json index 3104cac40..85f8bed39 100644 --- a/web/public/locales/ru/views/events.json +++ b/web/public/locales/ru/views/events.json @@ -37,5 +37,20 @@ "selected_other": "{{count}} выбрано", "detected": "обнаружен", "suspiciousActivity": "Подозрительная активность", - "threateningActivity": "Угрожающая активность" + "threateningActivity": "Угрожающая активность", + "detail": { + "noDataFound": "Нет данных для просмотра", + "aria": "Переключить подробный режим просмотра", + "trackedObject_one": "объект", + "trackedObject_other": "объекты", + "noObjectDetailData": "Данные о деталях объекта недоступны.", + "label": "Деталь", + "settings": "Настройки подробного просмотра" + }, + "objectTrack": { + "trackedPoint": "Отслеживаемая точка", + "clickToSeek": "Перейти к этому моменту" + }, + "zoomIn": "Увеличить", + "zoomOut": "Отдалить" } diff --git a/web/public/locales/ru/views/explore.json b/web/public/locales/ru/views/explore.json index 833dc3095..778ffd7d1 100644 --- a/web/public/locales/ru/views/explore.json +++ b/web/public/locales/ru/views/explore.json @@ -110,7 +110,8 @@ "details": "детали", "snapshot": "снимок", "video": "видео", - "object_lifecycle": "жизненный цикл объекта" + "object_lifecycle": "жизненный цикл объекта", + "thumbnail": "миниатюра" }, "objectLifecycle": { "title": "Жизненный цикл объекта", 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..3dcfd7cd5 100644 --- a/web/public/locales/ru/views/faceLibrary.json +++ b/web/public/locales/ru/views/faceLibrary.json @@ -12,7 +12,7 @@ "documentTitle": "Библиотека лиц - Frigate", "description": { "placeholder": "Введите название коллекции", - "addFace": "Пошаговое добавление новой коллекции в Библиотеку лиц.", + "addFace": "Добавьте новую коллекцию в библиотеку лиц, загрузив свое первое изображение.", "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчеркивания и дефисы." }, "createFaceLibrary": { @@ -28,8 +28,8 @@ }, "selectItem": "Выбор {{item}}", "train": { - "aria": "Выбор обучения", - "title": "Обучение", + "aria": "Выберите последние распознавания", + "title": "Последние распознавания", "empty": "Нет недавних попыток распознавания лиц" }, "toast": { diff --git a/web/public/locales/ru/views/live.json b/web/public/locales/ru/views/live.json index cd32e82eb..950a9b946 100644 --- a/web/public/locales/ru/views/live.json +++ b/web/public/locales/ru/views/live.json @@ -132,6 +132,9 @@ "playInBackground": { "label": "Воспроизвести в фоне", "tips": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "debug": { + "picker": "В режиме отладки выбор потока камеры недоступен. Вид отладчика всегда использует поток настроенный для режима обнаружения." } }, "cameraSettings": { diff --git a/web/public/locales/ru/views/settings.json b/web/public/locales/ru/views/settings.json index ef0021e1c..73a302ccd 100644 --- a/web/public/locales/ru/views/settings.json +++ b/web/public/locales/ru/views/settings.json @@ -10,7 +10,9 @@ "classification": "Настройки распознавания - Frigate", "object": "Отладка - Frigate", "notifications": "Настройки уведомлений - Frigate", - "enrichments": "Настройки обогащения - Frigate" + "enrichments": "Настройки обогащения - Frigate", + "cameraManagement": "Управление камерами - Frigate", + "cameraReview": "Настройки просмотра камеры - Frigate" }, "menu": { "cameras": "Настройки камеры", @@ -23,7 +25,10 @@ "ui": "Интерфейс", "classification": "Распознавание", "enrichments": "Обогащения", - "triggers": "Триггеры" + "triggers": "Триггеры", + "cameraManagement": "Управление", + "cameraReview": "Обзор", + "roles": "Роли" }, "dialog": { "unsavedChanges": { @@ -401,7 +406,7 @@ "name": { "title": "Название", "inputPlaceHolder": "Введите название…", - "tips": "Название должно содержать не менее 2 символов и не совпадать с названием камеры или другой зоны." + "tips": "Имя должно содержать не менее 2 символов, включать хотя бы одну букву и не совпадать с названием камеры или другой зоны." }, "inertia": { "title": "Инерция", @@ -815,6 +820,11 @@ "error": { "min": "Необходимо выбрать хотя бы одно действие." } + }, + "friendly_name": { + "description": "Необязательное название или описание к этому триггеру", + "placeholder": "Название или описание триггера", + "title": "Понятное название" } } }, @@ -829,6 +839,94 @@ "updateTriggerFailed": "Не удалось обновить триггер: {{errorMessage}}", "deleteTriggerFailed": "Не удалось удалить триггер: {{errorMessage}}" } + }, + "semanticSearch": { + "title": "Семантический поиск выключен", + "desc": "Для использования триггеров необходимо включить семантический поиск." + } + }, + "cameraWizard": { + "title": "Добавить камеру", + "description": "Следуйте инструкциям ниже, чтобы добавить новую камеру в вашу установку Frigate.", + "steps": { + "nameAndConnection": "Имя и подключение", + "streamConfiguration": "Конфигурация потока", + "validationAndTesting": "Проверка и тестирование" + }, + "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 в настройках камеры и перезапустите мастер настройки камеры." + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "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": "Тест потока не пройден" } } } diff --git a/web/public/locales/sk/audio.json b/web/public/locales/sk/audio.json index 511ea3846..4ea31df06 100644 --- a/web/public/locales/sk/audio.json +++ b/web/public/locales/sk/audio.json @@ -103,5 +103,401 @@ "toothbrush": "Zubná kefka", "roaring_cats": "Revúce mačky", "roar": "Revať", - "vehicle": "Vozidlo" + "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": "Whoosh", + "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 70765d857..a1a13eed1 100644 --- a/web/public/locales/sk/common.json +++ b/web/public/locales/sk/common.json @@ -89,11 +89,22 @@ "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äť" + "back": "Choď späť", + "hide": "Skryť {{item}}", + "show": "Zobraziť {{item}}", + "ID": "ID" }, "button": { "apply": "Použiť", @@ -127,9 +138,160 @@ "suspended": "Pozastavené", "export": "Exportovať", "deleteNow": "Odstrániť teraz", - "next": "Ďalej" + "next": "Ďalej", + "unsuspended": "Zrušte pozastavenie", + "play": "Hrať", + "unselect": "Zrušte výber" }, "menu": { - "system": "Systém" + "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ť" + }, + "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/dialog.json b/web/public/locales/sk/components/dialog.json index c1afb63f5..fe4ca101b 100644 --- a/web/public/locales/sk/components/dialog.json +++ b/web/public/locales/sk/components/dialog.json @@ -53,7 +53,7 @@ "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", @@ -108,7 +108,8 @@ "button": { "export": "Exportovať", "markAsReviewed": "Označiť ako skontrolované", - "deleteNow": "Odstrániť teraz" + "deleteNow": "Odstrániť teraz", + "markAsUnreviewed": "Označiť ako neskontrolované" } }, "imagePicker": { @@ -116,6 +117,7 @@ "search": { "placeholder": "Hľadať podľa štítku alebo podštítku..." }, - "noImages": "Pre tuto kameru sa nenašli žiadne miniatúry" + "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/objects.json b/web/public/locales/sk/objects.json index 7f0dca083..42ec664e2 100644 --- a/web/public/locales/sk/objects.json +++ b/web/public/locales/sk/objects.json @@ -90,5 +90,31 @@ "toothbrush": "Zubná kefka", "hair_brush": "Kefa na vlasy", "vehicle": "Vozidlo", - "squirrel": "Veverička" + "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..0442406fc --- /dev/null +++ b/web/public/locales/sk/views/classificationModel.json @@ -0,0 +1,152 @@ +{ + "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" + }, + "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": "" + }, + "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}}" + } + }, + "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." + }, + "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é" + } + }, + "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" + } +} diff --git a/web/public/locales/sk/views/events.json b/web/public/locales/sk/views/events.json index 7f886645c..59ab1eaf1 100644 --- a/web/public/locales/sk/views/events.json +++ b/web/public/locales/sk/views/events.json @@ -36,5 +36,24 @@ "camera": "Kamera", "detected": "Detekované", "suspiciousActivity": "Podozrivá aktivita", - "threateningActivity": "Ohrozujúca činnosť" + "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ť" } diff --git a/web/public/locales/sk/views/explore.json b/web/public/locales/sk/views/explore.json index ba860b422..e406dfa89 100644 --- a/web/public/locales/sk/views/explore.json +++ b/web/public/locales/sk/views/explore.json @@ -58,13 +58,24 @@ "camera": "Kamera", "zones": "Zóny", "button": { - "findSimilar": "Nájsť podobné" + "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." + "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" + "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": { @@ -100,7 +111,8 @@ "details": "detaily", "snapshot": "snímka", "video": "video", - "object_lifecycle": "životný cyklus objektu" + "object_lifecycle": "životný cyklus objektu", + "thumbnail": "Náhľad" }, "objectLifecycle": { "title": "Životný cyklus Objektu", @@ -150,5 +162,126 @@ "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í" + } + }, + "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}}" + } + } + }, + "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ť" + } + }, + "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..a390aab8d 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": "Sprievodca pridaním novej kolekcie do Knižnice tvárí.", + "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 e892ef122..fcc9df06d 100644 --- a/web/public/locales/sk/views/live.json +++ b/web/public/locales/sk/views/live.json @@ -90,8 +90,8 @@ "disable": "Skryť štatistiky streamu" }, "manualRecording": { - "title": "Nahrávanie na požiadanie", - "tips": "Spustiť manuálnu udalosť na základe nastavení uchovávania záznamu tejto kamery.", + "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ý." @@ -136,6 +136,9 @@ "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": { @@ -165,5 +168,16 @@ "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" + }, + "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/settings.json b/web/public/locales/sk/views/settings.json index 0d0a1fda7..424ce5f0c 100644 --- a/web/public/locales/sk/views/settings.json +++ b/web/public/locales/sk/views/settings.json @@ -9,7 +9,9 @@ "object": "Ladenie - Frigate", "general": "Všeobecné 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", @@ -21,7 +23,10 @@ "users": "Uživatelia", "notifications": "Notifikacie", "frigateplus": "Frigate+", - "triggers": "Spúšťače" + "triggers": "Spúšťače", + "roles": "Roly", + "cameraManagement": "Manažment", + "cameraReview": "Recenzia" }, "dialog": { "unsavedChanges": { @@ -44,6 +49,10 @@ "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." } }, "storedLayouts": { @@ -151,7 +160,921 @@ "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 " + "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" + }, + "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..." + } + }, + "step2": { + "description": "Konfigurovať prúdové role a pridať ďalšie prúdy pre vašu kameru.", + "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": "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." + } + }, + "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": "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é" + } + }, + "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": "{{count}} užívateľ (y) priradené tejto úlohe boli aktualizované pre \"viewer\", ktorý má prístup ku všetkým kamerám.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "" + }, + "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 2ff91c9ee..e2ea330e6 100644 --- a/web/public/locales/sk/views/system.json +++ b/web/public/locales/sk/views/system.json @@ -138,7 +138,49 @@ "capture": "zachytiť", "cameraFfmpeg": "{{camName}} FFmpeg", "cameraCapture": "zachytiť{{camName}}", - "cameraDetect": "Detekcia {{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" } } } diff --git a/web/public/locales/sl/components/dialog.json b/web/public/locales/sl/components/dialog.json index 46d1dfad8..f0284ee0f 100644 --- a/web/public/locales/sl/components/dialog.json +++ b/web/public/locales/sl/components/dialog.json @@ -32,7 +32,7 @@ }, "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", @@ -54,7 +54,7 @@ "export": "Izvoz", "selectOrExport": "Izberi ali Izvozi", "toast": { - "success": "Izvoz se je uspešno začel. Datoteko si oglejte v mapi /exports.", + "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", @@ -109,7 +109,8 @@ "button": { "export": "Izvoz", "markAsReviewed": "Označi kot pregledano", - "deleteNow": "Izbriši Zdaj" + "deleteNow": "Izbriši Zdaj", + "markAsUnreviewed": "Označi kot nepregledano" } }, "imagePicker": { diff --git a/web/public/locales/sl/components/filter.json b/web/public/locales/sl/components/filter.json index 1f4962b00..5a33b9709 100644 --- a/web/public/locales/sl/components/filter.json +++ b/web/public/locales/sl/components/filter.json @@ -129,6 +129,8 @@ "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." + "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..aceedb094 --- /dev/null +++ b/web/public/locales/sl/views/classificationModel.json @@ -0,0 +1,50 @@ +{ + "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" + }, + "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" + } + } +} diff --git a/web/public/locales/sl/views/faceLibrary.json b/web/public/locales/sl/views/faceLibrary.json index 9d5324759..be219cdf1 100644 --- a/web/public/locales/sl/views/faceLibrary.json +++ b/web/public/locales/sl/views/faceLibrary.json @@ -1,6 +1,6 @@ { "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." }, @@ -55,7 +55,8 @@ "createFaceLibrary": { "title": "Ustvari Zbirko", "desc": "Ustvari novo zbirko", - "new": "Ustvari Nov Obraz" + "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", @@ -66,8 +67,8 @@ } }, "train": { - "title": "Vlak", - "aria": "Izberite treniranje", + "title": "Nedavne prepoznave", + "aria": "Izberite nedavne prepoznave", "empty": "Ni nedavnih poskusov prepoznavanja obrazov" }, "selectItem": "Izberi {{item}}", @@ -93,7 +94,7 @@ "selectImage": "Izberite slikovno datoteko." }, "dropActive": "Sliko spustite tukaj…", - "dropInstructions": "Povlecite in spustite sliko sem ali kliknite za izbiro", + "dropInstructions": "Povlecite in spustite ali prilepite sliko sem ali kliknite za izbiro", "maxSize": "Največja velikost: {{size}}MB" }, "nofaces": "Noben obraz ni na voljo", diff --git a/web/public/locales/sl/views/settings.json b/web/public/locales/sl/views/settings.json index 14bdefc28..d8eff4e12 100644 --- a/web/public/locales/sl/views/settings.json +++ b/web/public/locales/sl/views/settings.json @@ -9,7 +9,9 @@ "general": "Splošne Nastavitve - Frigate", "frigatePlus": "Frigate+ Nastavitve - Frigate", "enrichments": "Nastavitve Obogatitev - Frigate", - "motionTuner": "Nastavitev gibanja - Frigate" + "motionTuner": "Nastavitev gibanja - Frigate", + "cameraManagement": "Upravljaj kamere - Frigate", + "cameraReview": "Nastavitve pregleda kamer – Frigate" }, "menu": { "ui": "Uporabniški vmesnik", @@ -21,7 +23,10 @@ "notifications": "Obvestila", "frigateplus": "Frigate+", "motionTuner": "Nastavitev Gibanja", - "triggers": "Prožilniki" + "triggers": "Prožilniki", + "cameraManagement": "Upravljanje", + "cameraReview": "Pregled", + "roles": "Vloge" }, "masksAndZones": { "zones": { @@ -86,7 +91,7 @@ "calendar": { "title": "Koledar", "firstWeekday": { - "label": "Prvi Delovni Dan", + "label": "Prvi dan v tednu", "desc": "Dan, na katerega se začnejo tedni v koledarju za preglede.", "sunday": "Nedelja", "monday": "Ponedeljek" @@ -203,5 +208,130 @@ "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/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/sv/audio.json b/web/public/locales/sv/audio.json index 6918ff2df..2de942a50 100644 --- a/web/public/locales/sv/audio.json +++ b/web/public/locales/sv/audio.json @@ -425,5 +425,79 @@ "television": "Tv", "radio": "Radio", "field_recording": "Fältinspelning", - "scream": "Skrika" + "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 8a783d6f8..2e9e1a627 100644 --- a/web/public/locales/sv/common.json +++ b/web/public/locales/sv/common.json @@ -250,7 +250,10 @@ "copyUrlToClipboard": "Webbadressen har kopierats till urklipp." }, "label": { - "back": "Gå tillbaka" + "back": "Gå tillbaka", + "hide": "Dölj {{item}}", + "show": "Visa {{item}}", + "ID": "ID" }, "unit": { "speed": { @@ -260,8 +263,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}}", - "readTheDocumentation": "Läs dokumentationen" + "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/dialog.json b/web/public/locales/sv/components/dialog.json index e41779b17..88ad466fa 100644 --- a/web/public/locales/sv/components/dialog.json +++ b/web/public/locales/sv/components/dialog.json @@ -52,7 +52,7 @@ "export": "Eksport", "selectOrExport": "Välj eller exportera", "toast": { - "success": "Exporten har startats. Visa filen i mappen /exports.", + "success": "Exporten har startats. Visa filen på exportsidan.", "error": { "failed": "Misslyckades med att starta exporten: {{error}}", "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", @@ -107,7 +107,8 @@ "button": { "export": "Exportera", "markAsReviewed": "Markera som granskad", - "deleteNow": "Ta bort nu" + "deleteNow": "Ta bort nu", + "markAsUnreviewed": "Markera som ogranskad" } }, "imagePicker": { @@ -115,6 +116,7 @@ "search": { "placeholder": "Sök efter etikett eller underetikett..." }, - "noImages": "Inga miniatyrbilder hittades för den här kameran" + "noImages": "Inga miniatyrbilder hittades för den här kameran", + "unknownLabel": "Sparad triggerbild" } } diff --git a/web/public/locales/sv/views/classificationModel.json b/web/public/locales/sv/views/classificationModel.json new file mode 100644 index 000000000..d7ee2ddfc --- /dev/null +++ b/web/public/locales/sv/views/classificationModel.json @@ -0,0 +1,163 @@ +{ + "documentTitle": "Klassificeringsmodeller", + "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" + }, + "error": { + "deleteImageFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteCategoryFailed": "Misslyckades med att ta bort klassen: {{errorMessage}}", + "categorizeFailed": "Misslyckades med att kategorisera bilden: {{errorMessage}}", + "trainingFailed": "Misslyckades med att starta modellträning: {{errorMessage}}", + "deleteModelFailed": "Misslyckades med att ta bort modellen: {{errorMessage}}", + "updateModelFailed": "Misslyckades med att uppdatera modell: {{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." + }, + "deleteDatasetImages": { + "title": "Ta bort datamängdsbilder", + "desc": "Ä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": "Ä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": "Nyligen" + }, + "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" + } + }, + "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" + } + }, + "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": "Är du säker på att du vill ta bort {{count}} modell(er)? 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." + }, + "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." + } +} diff --git a/web/public/locales/sv/views/events.json b/web/public/locales/sv/views/events.json index 89b9d1dd3..aed86c7ea 100644 --- a/web/public/locales/sv/views/events.json +++ b/web/public/locales/sv/views/events.json @@ -36,5 +36,24 @@ "selected_one": "{{count}} valda", "selected_other": "{{count}} valda", "suspiciousActivity": "Misstänkt aktivitet", - "threateningActivity": "Hotande aktivitet" + "threateningActivity": "Hotande aktivitet", + "detail": { + "noDataFound": "Inga detaljerade data att granska", + "aria": "Växla detaljvy", + "trackedObject_one": "objekt", + "trackedObject_other": "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" } diff --git a/web/public/locales/sv/views/explore.json b/web/public/locales/sv/views/explore.json index d4f3db8f9..b66acead6 100644 --- a/web/public/locales/sv/views/explore.json +++ b/web/public/locales/sv/views/explore.json @@ -109,7 +109,8 @@ "details": "detaljer", "video": "video", "snapshot": "ögonblicksbild", - "object_lifecycle": "objektets livscykel" + "object_lifecycle": "objektets livscykel", + "thumbnail": "miniatyrbild" }, "trackedObjectDetails": "Detaljer om spårade objekt", "objectLifecycle": { @@ -196,12 +197,22 @@ }, "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" } }, "dialog": { "confirmDelete": { "title": "Bekräfta radering", - "desc": "Om du tar bort det här spårade objektet tas bort ögonblicksbilden, alla sparade inbäddningar och alla tillhörande objektlivscykelposter 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?" + "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", @@ -222,5 +233,53 @@ }, "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}} vänster", + "heard": "{{label}} hördes", + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Förhållandet", + "area": "Område" + } + }, + "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. Starta om Frigate för att tillämpa dina ändringar." + } + } + }, + "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 7bb84d167..69c1536e4 100644 --- a/web/public/locales/sv/views/faceLibrary.json +++ b/web/public/locales/sv/views/faceLibrary.json @@ -11,8 +11,8 @@ }, "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": { @@ -26,12 +26,12 @@ "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", - "aria": "Välj träna", + "title": "Senaste Igenkänningar", + "aria": "Välj senaste igenkänningar", "empty": "Det finns inga ny försök till ansiktsigenkänning" }, "uploadFaceImage": { @@ -52,7 +52,7 @@ }, "imageEntry": { "dropActive": "Släpp bilden här…", - "dropInstructions": "Dra och släpp en bild här, eller klicka för att välja", + "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." diff --git a/web/public/locales/sv/views/live.json b/web/public/locales/sv/views/live.json index 17446420f..65b4667c1 100644 --- a/web/public/locales/sv/views/live.json +++ b/web/public/locales/sv/views/live.json @@ -82,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." @@ -170,5 +170,16 @@ "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" + }, + "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/settings.json b/web/public/locales/sv/views/settings.json index 1d477e645..0045d30bb 100644 --- a/web/public/locales/sv/views/settings.json +++ b/web/public/locales/sv/views/settings.json @@ -10,7 +10,9 @@ "frigatePlus": "Frigate+ Inställningar - Frigate", "notifications": "Notifikations Inställningar - Frigate", "motionTuner": "Rörelse inställning - Frigate", - "object": "Felsöka - Frigate" + "object": "Felsöka - Frigate", + "cameraManagement": "Hantera kameror - Frigate", + "cameraReview": "Kameragranskningsinställningar - Frigate" }, "general": { "title": "Allmänna Inställningar", @@ -20,10 +22,14 @@ "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." + } }, "storedLayouts": { "title": "Sparade Layouter", @@ -138,7 +144,10 @@ "enrichments": "Förbättringar", "motionTuner": "Rörelsemottagare", "debug": "Felsök", - "triggers": "Utlösare" + "triggers": "Utlösare", + "roles": "Roller", + "cameraManagement": "Hantering", + "cameraReview": "Granska" }, "dialog": { "unsavedChanges": { @@ -239,7 +248,8 @@ "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." + "hasIllegalCharacter": "Zonnamnet innehåller ogiltiga tecken.", + "mustHaveAtLeastOneLetter": "Zonnamnet måste ha minst en bokstav." } }, "distance": { @@ -294,7 +304,7 @@ "name": { "title": "Namn", "inputPlaceHolder": "Ange ett namn…", - "tips": "Namnet måste vara minst 2 tecken långt och får inte vara namnet på en kamera eller en annan zon." + "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 en annan zon." }, "inertia": { "title": "Momentum", @@ -634,7 +644,8 @@ "createRole": "Roll {{role}} skapad", "updateCameras": "Kameror uppdaterade för roll {{role}}", "deleteRole": "Roll {{role}} raderad", - "userRolesUpdated": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror." + "userRolesUpdated_one": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror.", + "userRolesUpdated_other": "" }, "error": { "createRoleFailed": "Misslyckades att skapa roll: {{errorMessage}}", @@ -725,7 +736,7 @@ "triggers": { "documentTitle": "Utlösare", "management": { - "title": "Utlösare hantering", + "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", @@ -746,7 +757,9 @@ }, "actions": { "notification": "Skicka avisering", - "alert": "Markera som varning" + "alert": "Markera som Varning", + "sub_label": "Lägg till underetikett", + "attribute": "Lägg till attribut" }, "dialog": { "createTrigger": { @@ -764,25 +777,28 @@ "form": { "name": { "title": "Namn", - "placeholder": "Ange utlösarens namn", + "placeholder": "Namnge denna utlösare", "error": { - "minLength": "Namnet måste vara minst 2 tecken lång.", - "invalidCharacters": "Namnet får bara innehålla bokstäver, siffror, understreck, och bindestreck.", + "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" + "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 bild", + "imagePlaceholder": "Välj en miniatyrbild", "textPlaceholder": "Ange textinnehåll", - "imageDesc": "Välj en bild för att utlösa den här åtgärden när en liknande bild upptäcks.", + "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." @@ -793,14 +809,20 @@ "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. Välj en ytterligare åtgärd att utföra när den här utlösaren utlöses.", + "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." } } }, @@ -815,6 +837,253 @@ "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" + }, + "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 testa anslutningen.", + "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..." + } + }, + "step2": { + "description": "Konfigurera strömningsroller och lägg till ytterligare strömmar för din kamera.", + "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": "Ström testet lyckades!", + "testFailed": "Ström testet 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ä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." + } + }, + "step3": { + "description": "Slutgiltig validering och analys innan du sparar din nya kamera. Anslut varje ström innan du sparar.", + "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." + } + }, + "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 d8f482898..87465b9e6 100644 --- a/web/public/locales/sv/views/system.json +++ b/web/public/locales/sv/views/system.json @@ -120,7 +120,7 @@ "resolution": "Upplösning:", "fps": "FPS:", "unknown": "Okänd", - "audio": "Audio:", + "audio": "Ljud:", "error": "Fel: {{error}}", "tips": { "title": "Kamera sond information" 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/th/views/classificationModel.json b/web/public/locales/th/views/classificationModel.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/web/public/locales/th/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/web/public/locales/tr/components/auth.json b/web/public/locales/tr/components/auth.json index dbc444b05..5d99dcd75 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 loglarında görüntülenir." } } diff --git a/web/public/locales/tr/components/dialog.json b/web/public/locales/tr/components/dialog.json index a4d7df98c..9b2e2e3f0 100644 --- a/web/public/locales/tr/components/dialog.json +++ b/web/public/locales/tr/components/dialog.json @@ -59,7 +59,7 @@ "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", diff --git a/web/public/locales/tr/views/classificationModel.json b/web/public/locales/tr/views/classificationModel.json new file mode 100644 index 000000000..82790b549 --- /dev/null +++ b/web/public/locales/tr/views/classificationModel.json @@ -0,0 +1,78 @@ +{ + "documentTitle": "Sınıflandırma Modelleri", + "details": { + "scoreInfo": "Skor, modelin nesneyi tespit ettiği tüm durumlar için ortalama güven düzeyini gösterir." + }, + "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" + }, + "toast": { + "success": { + "deletedCategory": "Silinmiş Sınıf", + "deletedImage": "Silinmiş Fotoğraflar", + "deletedModel_one": "{{tane}} model(ler) başarıyla silindi", + "deletedModel_other": "", + "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ı." + }, + "error": { + "deleteImageFailed": "Silinirken hatayla karşılaşıldı: {{errorMessage}}", + "deleteModelFailed": "Model silinirken hata oluştu: {{errorMessage}}", + "categorizeFailed": "Görsel sınıflandırılamadı: {{errorMessage}}", + "trainingFailed": "Model eğitimi başlatı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." + }, + "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": "{{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": "{{dataset}} veri kümesinden {{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve modelin yeniden eğitilmesini gerektirecektir." + }, + "deleteTrainImages": { + "title": "Eğitim Görsellerini Sil", + "desc": "{{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 etkili olabilmesi için modeli yeniden eğitmeniz gerekecektir." + }, + "description": { + "invalidName": "Geçersiz ad. Ad yalnızca harfler, rakamlar, boşluklar, kesme işaretleri (’), alt çizgiler (_) ve tireler (-) 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" + } + } +} diff --git a/web/public/locales/tr/views/events.json b/web/public/locales/tr/views/events.json index c1a821407..376ac03da 100644 --- a/web/public/locales/tr/views/events.json +++ b/web/public/locales/tr/views/events.json @@ -36,5 +36,23 @@ "selected_other": "{{count}} seçildi", "detected": "algılandı", "suspiciousActivity": "Şüpheli Etkinlik", - "threateningActivity": "Tehlikeli 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": "Nesne", + "trackedObject_other": "nesneler", + "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." + } + }, + "objectTrack": { + "trackedPoint": "Takip edilen nokta", + "clickToSeek": "Bu zamana gitmek için tıklayın" + } } diff --git a/web/public/locales/tr/views/explore.json b/web/public/locales/tr/views/explore.json index ce9466741..a12586cc4 100644 --- a/web/public/locales/tr/views/explore.json +++ b/web/public/locales/tr/views/explore.json @@ -109,7 +109,8 @@ "details": "detaylar", "object_lifecycle": "nesne yaşam döngüsü", "snapshot": "fotoğraf", - "video": "video" + "video": "video", + "thumbnail": "küçük resim" }, "objectLifecycle": { "title": "Nesne Yaşam Döngüsü", @@ -219,5 +220,32 @@ "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" + } + } } } 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..f3cfed89f 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 ad. Ad yalnızca harfler, rakamlar, boşluklar, kesme işaretleri (’), alt çizgiler (_) ve tireler (-) içerebilir." }, "details": { "person": "İnsan", @@ -24,11 +24,11 @@ "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ç", + "title": "Son Algılananlar", + "aria": "Son algılanan nesneleri seç", "empty": "Yakın zamanda yüz tanıma denemesi olmadı" }, "deleteFaceLibrary": { @@ -49,7 +49,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": { diff --git a/web/public/locales/tr/views/live.json b/web/public/locales/tr/views/live.json index ae3b0cd1c..ad1704ab7 100644 --- a/web/public/locales/tr/views/live.json +++ b/web/public/locales/tr/views/live.json @@ -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", + "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." @@ -167,5 +167,11 @@ "transcription": { "enable": "Canlı Ses Çözümlemeyi Aç", "disable": "Canlı Ses Çözümlemeyi Kapat" + }, + "snapshot": { + "takeSnapshot": "Anlık Ekran Görüntüsünü İndir", + "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ı." } } diff --git a/web/public/locales/tr/views/settings.json b/web/public/locales/tr/views/settings.json index 714f6a4fc..a9c801962 100644 --- a/web/public/locales/tr/views/settings.json +++ b/web/public/locales/tr/views/settings.json @@ -10,7 +10,9 @@ "object": "Hata Ayıklama - Frigate", "general": "Genel 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", @@ -23,7 +25,10 @@ "debug": "Hata Ayıklama", "cameras": "Kamera Ayarları", "enrichments": "Zenginleştirmeler", - "triggers": "Tetikler" + "triggers": "Tetikler", + "cameraManagement": "Yönet", + "cameraReview": "İncele", + "roles": "Roller" }, "general": { "title": "Genel Ayarlar", @@ -36,7 +41,11 @@ "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." }, - "title": "Canlı Görüntü Paneli" + "title": "Canlı Görüntü Paneli", + "displayCameraNames": { + "label": "Kamera Adlarını Her Zaman Göster", + "desc": "Çok kameralı canlı izleme panelinde, kamera adlarını her zaman bir etiket içinde göster." + } }, "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.", 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 542ebb27a..a3338a223 100644 --- a/web/public/locales/uk/common.json +++ b/web/public/locales/uk/common.json @@ -227,10 +227,21 @@ "length": { "feet": "ноги", "meters": "метрів" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/годину", + "mbph": "МБ/годину", + "gbph": "ГБ/годину" } }, "label": { - "back": "Повернутись" + "back": "Повернутись", + "hide": "Приховати {{item}}", + "show": "Показати {{item}}", + "ID": "ID" }, "toast": { "save": { @@ -271,5 +282,17 @@ "title": "404" }, "selectItem": "Вибрати {{item}}", - "readTheDocumentation": "Прочитати документацію" + "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/dialog.json b/web/public/locales/uk/components/dialog.json index fadbb19e0..762eea9ed 100644 --- a/web/public/locales/uk/components/dialog.json +++ b/web/public/locales/uk/components/dialog.json @@ -57,7 +57,7 @@ "endTimeMustAfterStartTime": "Час закінчення повинен бути після часу початку", "noVaildTimeSelected": "Не вибрано допустимий діапазон часу" }, - "success": "Експорт успішно запущено. Файл доступний у теці /exports." + "success": "Експорт успішно розпочато. Перегляньте файл на сторінці експорту." }, "fromTimeline": { "saveExport": "Зберегти експорт", @@ -89,7 +89,8 @@ "button": { "export": "Експорт", "markAsReviewed": "Позначити як переглянуте", - "deleteNow": "Вилучити зараз" + "deleteNow": "Вилучити зараз", + "markAsUnreviewed": "Позначити як непереглянуте" }, "confirmDelete": { "title": "Підтвердити вилучення", @@ -116,6 +117,7 @@ "search": { "placeholder": "Пошук за міткою або підміткою..." }, - "noImages": "Для цієї камери не знайдено мініатюр" + "noImages": "Для цієї камери не знайдено мініатюр", + "unknownLabel": "Збережене зображення тригера" } } diff --git a/web/public/locales/uk/views/classificationModel.json b/web/public/locales/uk/views/classificationModel.json new file mode 100644 index 000000000..943aebba3 --- /dev/null +++ b/web/public/locales/uk/views/classificationModel.json @@ -0,0 +1,164 @@ +{ + "documentTitle": "Моделі класифікації", + "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": "Конфігурацію моделі успішно оновлено" + }, + "error": { + "deleteImageFailed": "Не вдалося видалити: {{errorMessage}}", + "deleteCategoryFailed": "Не вдалося видалити клас: {{errorMessage}}", + "categorizeFailed": "Не вдалося класифікувати зображення: {{errorMessage}}", + "trainingFailed": "Не вдалося розпочати навчання моделі: {{errorMessage}}", + "deleteModelFailed": "Не вдалося видалити модель: {{errorMessage}}", + "updateModelFailed": "Не вдалося оновити модель: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Видалити клас", + "desc": "Ви впевнені, що хочете видалити клас {{name}}? Це назавжди видалить усі пов'язані зображення та вимагатиме повторного навчання моделі." + }, + "deleteDatasetImages": { + "title": "Видалити зображення набору даних", + "desc": "Ви впевнені, що хочете видалити {{count}} зображень з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі." + }, + "deleteTrainImages": { + "title": "Видалити зображення поїздів", + "desc": "Ви впевнені, що хочете видалити {{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": "Будь ласка, виберіть тип класифікації" + }, + "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": "Зразки зображень успішно створено" + } + }, + "deleteModel": { + "title": "Видалити модель класифікації", + "single": "Ви впевнені, що хочете видалити {{name}}? Це назавжди видалить усі пов’язані дані, включаючи зображення та дані навчання. Цю дію не можна скасувати.", + "desc": "Ви впевнені, що хочете видалити {{count}} модель(і)? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати." + }, + "menu": { + "objects": "Об'єкти", + "states": "Стани" + }, + "details": { + "scoreInfo": "Оцінка представляє середню достовірність класифікації для всіх виявлень цього об'єкта." + }, + "edit": { + "title": "Редагувати модель класифікації", + "descriptionState": "Відредагуйте класи для цієї моделі класифікації штатів. Зміни вимагатимуть перенавчання моделі.", + "descriptionObject": "Відредагуйте тип об'єкта та тип класифікації для цієї моделі класифікації об'єктів.", + "stateClassesInfo": "Примітка: Зміна класів станів вимагає перенавчання моделі з використанням оновлених класів." + } +} diff --git a/web/public/locales/uk/views/events.json b/web/public/locales/uk/views/events.json index 4496b3915..993933d6c 100644 --- a/web/public/locales/uk/views/events.json +++ b/web/public/locales/uk/views/events.json @@ -36,5 +36,24 @@ }, "detected": "виявлено", "suspiciousActivity": "Підозріла активність", - "threateningActivity": "Загрозлива діяльність" + "threateningActivity": "Загрозлива діяльність", + "detail": { + "noDataFound": "Немає детальних даних для перегляду", + "aria": "Перемикання детального перегляду", + "trackedObject_one": "об'єкт", + "trackedObject_other": "об'єкти", + "noObjectDetailData": "Детальні дані про об'єкт недоступні.", + "label": "Деталь", + "settings": "Налаштування детального перегляду", + "alwaysExpandActive": { + "title": "Завжди розгортати активне", + "desc": "Завжди розгортайте деталі об'єкта активного елемента огляду, якщо вони доступні." + } + }, + "objectTrack": { + "trackedPoint": "Відстежувана Точка", + "clickToSeek": "Натисніть, щоб перейти до цього часу" + }, + "zoomIn": "Збільшити масштаб", + "zoomOut": "Зменшити масштаб" } diff --git a/web/public/locales/uk/views/explore.json b/web/public/locales/uk/views/explore.json index f4691c2ee..5d90544a6 100644 --- a/web/public/locales/uk/views/explore.json +++ b/web/public/locales/uk/views/explore.json @@ -168,7 +168,7 @@ "dialog": { "confirmDelete": { "title": "Підтвердити видалення", - "desc": "Видалення цього відстежуваного об’єкта призведе до видалення знімка, будь-яких збережених вбудованих елементів та будь-яких пов’язаних записів життєвого циклу об’єкта. Записані кадри цього відстежуваного об’єкта в режимі перегляду історії НЕ будуть видалені.

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

    Ви впевнені, що хочете продовжити?" } }, "itemMenu": { @@ -206,6 +206,16 @@ "audioTranscription": { "label": "Транскрибувати", "aria": "Запит на аудіотранскрипцію" + }, + "viewTrackingDetails": { + "label": "Переглянути деталі відстеження", + "aria": "Показати деталі відстеження" + }, + "showObjectDetails": { + "label": "Показати шлях до об'єкта" + }, + "hideObjectDetails": { + "label": "Приховати шлях до об'єкта" } }, "noTrackedObjects": "Відстежуваних об'єктів не знайдено", @@ -216,7 +226,8 @@ "details": "деталі", "snapshot": "знімок", "video": "відео", - "object_lifecycle": "життєвий цикл об'єкта" + "object_lifecycle": "життєвий цикл об'єкта", + "thumbnail": "мініатюра" }, "exploreMore": "Дослідіть більше об'єктів {{label}}", "aiAnalysis": { @@ -224,5 +235,53 @@ }, "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": "Площа" + } + }, + "annotationSettings": { + "title": "Налаштування анотацій", + "showAllZones": { + "title": "Показати всі зони", + "desc": "Завжди показувати зони на кадрах, де об'єкти увійшли в зону." + }, + "offset": { + "label": "Зсув анотації", + "desc": "Ці дані надходять із каналу виявлення вашої камери, але накладаються на зображення з каналу запису. Малоймовірно, що ці два потоки будуть ідеально синхронізовані. Як результат, обмежувальна рамка та відеоматеріал не будуть ідеально збігатися. Ви можете використовувати це налаштування, щоб змістити анотації вперед або назад у часі, щоб краще узгодити їх із записаним відеоматеріалом.", + "millisecondsToOffset": "Мілісекунди для зміщення виявлених анотацій. За замовчуванням: 0", + "tips": "ПІДКАЗКА: Уявіть, що є кліп події, на якому людина йде зліва направо. Якщо обмежувальний прямокутник часової шкали події постійно знаходиться ліворуч від людини, то значення слід зменшити. Аналогічно, якщо людина йде зліва направо, а обмежувальний прямокутник постійно знаходиться попереду людини, то значення слід збільшити.", + "toast": { + "success": "Зміщення анотації для {{camera}} збережено у файлі конфігурації. Перезапустіть Frigate, щоб застосувати зміни." + } + } + }, + "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..621dc0e8b 100644 --- a/web/public/locales/uk/views/faceLibrary.json +++ b/web/public/locales/uk/views/faceLibrary.json @@ -66,12 +66,12 @@ "selectImage": "Будь ласка, виберіть файл зображення." }, "dropActive": "Скинь зображення сюди…", - "dropInstructions": "Перетягніть зображення сюди або клацніть, щоб вибрати" + "dropInstructions": "Перетягніть або вставте зображення сюди, або клацніть, щоб вибрати" }, "trainFaceAs": "Тренуйте обличчя як:", "trainFace": "Обличчя поїзда", "description": { - "addFace": "Покрокові інструкції з додавання нової колекції до Бібліотеки облич.", + "addFace": "Додайте нову колекцію до Бібліотеки облич, завантаживши своє перше зображення.", "placeholder": "Введіть назву для цієї колекції", "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." }, @@ -83,11 +83,11 @@ "title": "Створити колекцію", "desc": "Створити нову колекцію", "new": "Створити нове обличчя", - "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Навчання», щоб вибрати та навчити зображення для кожної виявленої особи.
  • Для найкращих результатів зосередьтеся на зображеннях, спрямованих прямо в обличчя; уникайте навчальних зображень, які фіксують обличчя під кутом.
  • " + "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Недавні розпізнавання», щоб вибрати та навчити систему розпізнавати зображення для кожної виявленої особи.
  • Для досягнення найкращих результатів зосередьтеся на прямих зображеннях; уникайте навчання зображень, на яких обличчя зняті під кутом.
  • " }, "train": { - "title": "Поїзд", - "aria": "Виберіть поїзд", + "title": "Нещодавні визнання", + "aria": "Виберіть нещодавні визнання", "empty": "Немає останніх спроб розпізнавання обличчя" }, "collections": "Колекції", diff --git a/web/public/locales/uk/views/live.json b/web/public/locales/uk/views/live.json index 8b2248ffc..8562f5115 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": "Не вдалося запустити ручний запис на вимогу.", @@ -170,5 +170,16 @@ "transcription": { "enable": "Увімкнути транскрипцію аудіо в реальному часі", "disable": "Вимкнути транскрипцію аудіо в реальному часі" + }, + "noCameras": { + "title": "Немає налаштованих камер", + "description": "Почніть з підключення камери до Frigate.", + "buttonText": "Додати камеру" + }, + "snapshot": { + "takeSnapshot": "Завантажити миттєвий знімок", + "noVideoSource": "Немає доступного джерела відео для знімка.", + "captureFailed": "Не вдалося зробити знімок.", + "downloadStarted": "Розпочато завантаження знімка." } } diff --git a/web/public/locales/uk/views/settings.json b/web/public/locales/uk/views/settings.json index 24ff6db8b..965cc8440 100644 --- a/web/public/locales/uk/views/settings.json +++ b/web/public/locales/uk/views/settings.json @@ -161,7 +161,7 @@ "name": { "inputPlaceHolder": "Введіть назву…", "title": "Ім'я", - "tips": "Назва має містити щонайменше 2 символи та не повинна бути назвою камери чи іншої зони." + "tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони." }, "desc": { "title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.", @@ -252,7 +252,8 @@ "mustNotContainPeriod": "Назва зони не повинна містити крапок.", "mustNotBeSameWithCamera": "Назва зони не повинна збігатися з назвою камери.", "mustBeAtLeastTwoCharacters": "Назва зони має містити щонайменше 2 символи.", - "hasIllegalCharacter": "Назва зони містить недопустимі символи." + "hasIllegalCharacter": "Назва зони містить недопустимі символи.", + "mustHaveAtLeastOneLetter": "Назва зони повинна містити щонайменше одну літеру." } }, "polygonDrawing": { @@ -487,6 +488,10 @@ "playAlertVideos": { "label": "Відтворити відео зі сповіщеннями", "desc": "За замовчуванням останні сповіщення на панелі керування Live відтворюються як невеликі відеозаписи, що циклічно відтворюються. Вимкніть цю опцію, щоб відображати лише статичне зображення останніх сповіщень на цьому пристрої/у браузері." + }, + "displayCameraNames": { + "label": "Завжди показувати назви камер", + "desc": "Завжди відображати назви камер у чіпі на панелі керування режимом живого перегляду з кількох камер." } }, "storedLayouts": { @@ -549,9 +554,11 @@ "classification": "Налаштування класифікації – Фрегат", "masksAndZones": "Редактор масок та зон – Фрегат", "motionTuner": "Тюнер руху - Фрегат", - "general": "Основна Налаштуваннях – Frigate", + "general": "Основна Налаштування – Frigate", "frigatePlus": "Налаштування Frigate+ – Frigate", - "enrichments": "Налаштуваннях збагачення – Frigate" + "enrichments": "Налаштуваннях збагачення – Frigate", + "cameraManagement": "Керування камерами - Frigate", + "cameraReview": "Налаштування перегляду камери - Frigate" }, "menu": { "ui": "Інтерфейс користувача", @@ -563,8 +570,11 @@ "debug": "Налагодження", "notifications": "Сповіщення", "frigateplus": "Frigate+", - "enrichments": "Збагачення", - "triggers": "Тригери" + "enrichments": "Збагаченням", + "triggers": "Тригери", + "roles": "Ролі", + "cameraManagement": "Управління", + "cameraReview": "Огляду" }, "dialog": { "unsavedChanges": { @@ -739,7 +749,7 @@ "triggers": { "documentTitle": "Тригери", "management": { - "title": "Управління тригерами", + "title": "Тригери", "desc": "Керуйте тригерами для {{camera}}. Використовуйте тип мініатюри для спрацьовування на схожих мініатюрах до вибраного об’єкта відстеження, а тип опису – для спрацьовування на схожих описах до вказаного вами тексту." }, "addTrigger": "Додати Тригер", @@ -760,7 +770,9 @@ }, "actions": { "alert": "Позначити як сповіщення", - "notification": "Надіслати сповіщення" + "notification": "Надіслати сповіщення", + "sub_label": "Додати підмітку", + "attribute": "Додати атрибут" }, "dialog": { "createTrigger": { @@ -778,25 +790,28 @@ "form": { "name": { "title": "Ім'я", - "placeholder": "Введіть назву тригера", + "placeholder": "Назвіть цей тригер", "error": { - "minLength": "Ім'я має містити щонайменше 2 символи.", - "invalidCharacters": "Ім'я може містити лише літери, цифри, символи підкреслення та дефіси.", + "minLength": "Поле має містити щонайменше 2 символи.", + "invalidCharacters": "Поле може містити лише літери, цифри, символи підкреслення та дефіси.", "alreadyExists": "Тригер із такою назвою вже існує для цієї камери." - } + }, + "description": "Введіть унікальну назву або опис, щоб ідентифікувати цей тригер" }, "enabled": { "description": "Увімкнути або вимкнути цей тригер" }, "type": { "title": "Тип", - "placeholder": "Виберіть тип тригера" + "placeholder": "Виберіть тип тригера", + "description": "Спрацьовує, коли виявляється схожий опис відстежуваного об'єкта", + "thumbnail": "Спрацьовує, коли виявляється мініатюра схожого відстежуваного об'єкта" }, "content": { "title": "Зміст", - "imagePlaceholder": "Виберіть зображення", + "imagePlaceholder": "Виберіть мініатюру", "textPlaceholder": "Введіть текстовий вміст", - "imageDesc": "Виберіть зображення, щоб запустити цю дію, коли буде виявлено схоже зображення.", + "imageDesc": "Відображаються лише 100 останніх мініатюр. Якщо ви не можете знайти потрібну мініатюру, перегляньте попередні об’єкти в розділі «Огляд» і налаштуйте тригер у меню.", "textDesc": "Введіть текст, щоб запустити цю дію, коли буде виявлено схожий опис відстежуваного об’єкта.", "error": { "required": "Контент обов'язковий." @@ -807,14 +822,20 @@ "error": { "min": "Поріг має бути щонайменше 0", "max": "Поріг має бути не більше 1" - } + }, + "desc": "Встановіть поріг подібності для цього тригера. Вищий поріг означає, що для спрацьовування тригера потрібна ближча відповідність." }, "actions": { "title": "Дії", - "desc": "За замовчуванням Frigate надсилає повідомлення MQTT для всіх тригерів. Виберіть додаткову дію, яку потрібно виконати, коли цей тригер спрацьовує.", + "desc": "За замовчуванням Frigate надсилає повідомлення MQTT для всіх тригерів. Підмітки додають назву тригера до мітки об'єкта. Атрибути – це метадані, які можна шукати, що зберігаються окремо в метаданих відстежуваного об'єкта.", "error": { "min": "Потрібно вибрати принаймні одну дію." } + }, + "friendly_name": { + "title": "Зрозуміле ім'я", + "placeholder": "Назвіть або опишіть цей тригер", + "description": "Зрозуміла назва або описовий текст (необов'язково) для цього тригера." } } }, @@ -829,6 +850,27 @@ "updateTriggerFailed": "Не вдалося оновити тригер: {{errorMessage}}", "deleteTriggerFailed": "Не вдалося видалити тригер: {{errorMessage}}" } + }, + "semanticSearch": { + "title": "Семантичний пошук вимкнено", + "desc": "Для використання тригерів необхідно ввімкнути семантичний пошук." + }, + "wizard": { + "title": "Створити тригер", + "step1": { + "description": "Налаштуйте основні параметри для вашого тригера." + }, + "step2": { + "description": "Налаштуйте контент, який запускатиме цю дію." + }, + "step3": { + "description": "Налаштуйте поріг та дії для цього тригера." + }, + "steps": { + "nameAndType": "Ім'я та тип", + "configureData": "Налаштувати дані", + "thresholdAndActions": "Поріг та дії" + } } }, "roles": { @@ -846,7 +888,9 @@ "createRole": "Роль {{role}} успішно створена", "updateCameras": "Камери оновлено для ролі {{role}}", "deleteRole": "Роль {{role}} успішно видалено", - "userRolesUpdated": "Користувачів ({{count}}), яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер." + "userRolesUpdated_one": "Користувачів ({{count}}), яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер.", + "userRolesUpdated_few": "", + "userRolesUpdated_many": "" }, "error": { "createRoleFailed": "Не вдалося створити роль: {{errorMessage}}", @@ -890,5 +934,231 @@ } } } + }, + "cameraWizard": { + "title": "Додати камеру", + "description": "Виконайте наведені нижче кроки, щоб додати нову камеру до вашої установки Frigate.", + "steps": { + "nameAndConnection": "Ім'я та з'єднання", + "streamConfiguration": "Конфігурація потоку", + "validationAndTesting": "Валідація та тестування" + }, + "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": "Отримання знімка камери..." + } + }, + "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, щоб зменшити кількість підключень до вашої камери." + } + }, + "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. Це може забезпечити кращу сумісність з деякими потоками камер." + } + }, + "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 5898da3eb..43e8cfcb0 100644 --- a/web/public/locales/uk/views/system.json +++ b/web/public/locales/uk/views/system.json @@ -151,7 +151,7 @@ "documentTitle": { "cameras": "Статистика камер - Фрегат", "storage": "Статистика сховища - Фрегат", - "general": "Загальна статистика - Frigate", + "general": "Основна Статус – Frigate", "enrichments": "Статистика збагачені - Фрегат", "logs": { "frigate": "Фрегатні журнали - Фрегат", 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/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/views/classificationModel.json b/web/public/locales/vi/views/classificationModel.json new file mode 100644 index 000000000..1f0cf85be --- /dev/null +++ b/web/public/locales/vi/views/classificationModel.json @@ -0,0 +1,20 @@ +{ + "documentTitle": "Mô Hình Phân Loại", + "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" + }, + "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" + } + } +} diff --git a/web/public/locales/vi/views/events.json b/web/public/locales/vi/views/events.json index c3bdad497..c85f6cfdc 100644 --- a/web/public/locales/vi/views/events.json +++ b/web/public/locales/vi/views/events.json @@ -36,5 +36,6 @@ "markAsReviewed": "Đánh dấu là đã xem xét", "markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét", "suspiciousActivity": "Hoạt động đáng ngờ", - "threateningActivity": "Hoạt động đe dọa" + "threateningActivity": "Hoạt động đe dọa", + "zoomIn": "Phóng To" } diff --git a/web/public/locales/vi/views/exports.json b/web/public/locales/vi/views/exports.json index 6206f5821..6ae992551 100644 --- a/web/public/locales/vi/views/exports.json +++ b/web/public/locales/vi/views/exports.json @@ -13,5 +13,10 @@ "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" } } diff --git a/web/public/locales/vi/views/settings.json b/web/public/locales/vi/views/settings.json index 03b17d4ae..267948102 100644 --- a/web/public/locales/vi/views/settings.json +++ b/web/public/locales/vi/views/settings.json @@ -9,7 +9,9 @@ "object": "Gỡ lỗi - Frigate", "general": "Cài đặt Chung - 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": { diff --git a/web/public/locales/yue-Hant/common.json b/web/public/locales/yue-Hant/common.json index 60bb2a2c6..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": { @@ -249,5 +265,8 @@ "desc": "找不到頁面", "title": "404" }, - "readTheDocumentation": "閱讀文件" + "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..369482406 100644 --- a/web/public/locales/zh-CN/audio.json +++ b/web/public/locales/zh-CN/audio.json @@ -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 2c028bcb2..d4e3aba21 100644 --- a/web/public/locales/zh-CN/common.json +++ b/web/public/locales/zh-CN/common.json @@ -85,10 +85,21 @@ "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" }, "pagination": { "label": "分页", @@ -266,5 +277,17 @@ "desc": "页面未找到" }, "selectItem": "选择 {{item}}", - "readTheDocumentation": "阅读文档" + "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..cb8a9b802 100644 --- a/web/public/locales/zh-CN/components/camera.json +++ b/web/public/locales/zh-CN/components/camera.json @@ -66,7 +66,8 @@ }, "stream": "视频流", "placeholder": "选择视频流" - } + }, + "birdseye": "鸟瞰图" } }, "debug": { diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json index 3cc58c541..db2af6069 100644 --- a/web/public/locales/zh-CN/components/dialog.json +++ b/web/public/locales/zh-CN/components/dialog.json @@ -59,7 +59,7 @@ "export": "导出", "selectOrExport": "选择或导出", "toast": { - "success": "导出成功。进入 /exports 目录查看文件。", + "success": "导出成功。进入 导出 页面查看文件。", "error": { "failed": "导出失败:{{error}}", "endTimeMustAfterStartTime": "结束时间必须在开始时间之后", @@ -114,7 +114,8 @@ "button": { "export": "导出", "markAsReviewed": "标记为已核查", - "deleteNow": "立即删除" + "deleteNow": "立即删除", + "markAsUnreviewed": "标记为未核查" } }, "imagePicker": { @@ -122,6 +123,7 @@ "search": { "placeholder": "通过标签或子标签搜索……" }, - "noImages": "未在此摄像头找到缩略图" + "noImages": "未在此摄像头找到缩略图", + "unknownLabel": "已保存触发的图片" } } diff --git a/web/public/locales/zh-CN/components/filter.json b/web/public/locales/zh-CN/components/filter.json index 42a732f8e..52341c68f 100644 --- a/web/public/locales/zh-CN/components/filter.json +++ b/web/public/locales/zh-CN/components/filter.json @@ -122,7 +122,9 @@ "loading": "正在加载识别的车牌…", "placeholder": "输入以搜索车牌…", "noLicensePlatesFound": "未找到车牌。", - "selectPlatesFromList": "从列表中选择一个或多个车牌。" + "selectPlatesFromList": "从列表中选择一个或多个车牌。", + "selectAll": "选择所有", + "clearAll": "清除所有" }, "classes": { "label": "分类", 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..6d59c1431 --- /dev/null +++ b/web/public/locales/zh-CN/views/classificationModel.json @@ -0,0 +1,162 @@ +{ + "documentTitle": "分类模型", + "button": { + "deleteClassificationAttempts": "删除分类图片", + "renameCategory": "重命名类别", + "deleteCategory": "删除类别", + "deleteImages": "删除图片", + "trainModel": "训练模型", + "addClassification": "添加分类", + "deleteModels": "删除模型", + "editModel": "编辑模型" + }, + "toast": { + "success": { + "deletedCategory": "删除类别", + "deletedImage": "删除图片", + "categorizedImage": "成功分类图片", + "trainedModel": "训练模型成功。", + "trainingModel": "已开始训练模型。", + "deletedModel_other": "已删除 {{count}} 个模型", + "updatedModel": "已更新模型配置" + }, + "error": { + "deleteImageFailed": "删除失败:{{errorMessage}}", + "deleteCategoryFailed": "删除类别失败:{{errorMessage}}", + "categorizeFailed": "图片分类失败:{{errorMessage}}", + "trainingFailed": "开始训练模型失败:{{errorMessage}}", + "deleteModelFailed": "删除模型失败:{{errorMessage}}", + "updateModelFailed": "更新模型失败:{{errorMessage}}" + } + }, + "deleteCategory": { + "title": "删除类别", + "desc": "确定要删除类别 {{name}} 吗?此操作将永久删除所有关联的图片,并需要重新训练模型。" + }, + "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": "请选择一个目标标签" + }, + "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": "样本图片生成成功" + } + }, + "deleteModel": { + "title": "删除分类模型", + "single": "你确定要删除 {{name}} 吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。", + "desc": "你确定要删除 {{count}} 个模型吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。" + }, + "menu": { + "objects": "目标", + "states": "状态" + }, + "details": { + "scoreInfo": "得分表示该目标所有检测结果的平均分类置信度。" + }, + "edit": { + "title": "编辑分类模型", + "descriptionState": "编辑此状态分类模型的类别;更改后需要重新训练模型。", + "descriptionObject": "编辑此目标分类模型的目标类型和分类类型。", + "stateClassesInfo": "注意:更改状态类别后需使用更新后的类别重新训练模型。" + } +} diff --git a/web/public/locales/zh-CN/views/events.json b/web/public/locales/zh-CN/views/events.json index 71d00848b..8269da7d7 100644 --- a/web/public/locales/zh-CN/views/events.json +++ b/web/public/locales/zh-CN/views/events.json @@ -37,5 +37,24 @@ "selected_other": "已选择 {{count}} 个", "detected": "已检测", "suspiciousActivity": "可疑活动", - "threateningActivity": "威胁性活动" + "threateningActivity": "风险类活动", + "detail": { + "noDataFound": "没有可供核查的详细数据", + "aria": "切换详细视图", + "trackedObject_one": "目标或物体", + "trackedObject_other": "目标或物体", + "noObjectDetailData": "没有目标详细信息。", + "label": "详细信息", + "settings": "详细视图设置", + "alwaysExpandActive": { + "title": "始终展开当前项", + "desc": "在可用情况下,将始终展开当前核查项的对象详细信息。" + } + }, + "objectTrack": { + "trackedPoint": "追踪点", + "clickToSeek": "点击从该时间进行寻找" + }, + "zoomIn": "放大", + "zoomOut": "缩小" } diff --git a/web/public/locales/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json index a81fa96f4..45dcd46e8 100644 --- a/web/public/locales/zh-CN/views/explore.json +++ b/web/public/locales/zh-CN/views/explore.json @@ -34,7 +34,8 @@ "details": "详情", "snapshot": "快照", "video": "视频", - "object_lifecycle": "目标全周期" + "object_lifecycle": "目标全周期", + "thumbnail": "缩略图" }, "objectLifecycle": { "title": "目标全周期", @@ -194,12 +195,22 @@ "audioTranscription": { "label": "转录", "aria": "请求音频转录" + }, + "showObjectDetails": { + "label": "显示目标轨迹" + }, + "hideObjectDetails": { + "label": "隐藏目标轨迹" + }, + "viewTrackingDetails": { + "label": "查看追踪详情", + "aria": "显示追踪详情" } }, "dialog": { "confirmDelete": { "title": "确认删除", - "desc": "删除此追踪目标后将移除快照、所有已保存的嵌入向量数据以及任何相关的目标全周期条目,但在 历史 页面中跟踪对象的录制视频片段不会被删除。

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

    你确定要继续删除该追踪目标吗?" } }, "noTrackedObjects": "未找到追踪目标", @@ -217,5 +228,56 @@ "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": "坐标区域" + } + }, + "annotationSettings": { + "title": "标记设置", + "showAllZones": { + "title": "显示所有区", + "desc": "在目标进入区域的帧中始终显示区域框。" + }, + "offset": { + "label": "标记偏移量", + "desc": "此数据来自摄像头的检测视频流,但叠加在录制视频流的画面上。两个视频流可能不会完全同步,因此边框与画面可能无法完全对齐。可以使用此设置将标记在时间轴上向前或向后偏移,以更好地与录制画面对齐。", + "millisecondsToOffset": "用于偏移检测标记的毫秒数。 默认值:0", + "tips": "提示:假设有一段人从左向右走的事件录制,如果事件时间轴中的边框始终在人的左侧(即后方),则应该减小偏移值;反之,如果边框始终领先于人物,则应增大偏移值。", + "toast": { + "success": "{{camera}} 的标记偏移量已保存。请重启 Frigate 以应用更改。" + } + } + }, + "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 66eb4e204..d43b1c366 100644 --- a/web/public/locales/zh-CN/views/faceLibrary.json +++ b/web/public/locales/zh-CN/views/faceLibrary.json @@ -23,11 +23,11 @@ "title": "创建特征库", "desc": "创建一个新的特征库", "new": "新建人脸", - "nextSteps": "建议按以下步骤建立可靠的特征库:
  • 使用训练选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " + "nextSteps": "建议按以下步骤建立可靠的特征库:
  • 使用近期识别记录选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " }, "train": { - "title": "训练", - "aria": "选择训练", + "title": "近期识别记录", + "aria": "选择近期识别记录", "empty": "近期未检测到人脸识别操作" }, "selectItem": "选择 {{item}}", @@ -49,7 +49,7 @@ "selectImage": "请选择图片文件。" }, "dropActive": "拖动图片文件到这里…", - "dropInstructions": "拖动图片文件到此处或点击选择", + "dropInstructions": "拖动或粘贴图片文件到此处,也可以点击选择文件", "maxSize": "最大文件大小:{{size}}MB" }, "readTheDocs": "阅读文档", diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json index 372c200e0..7c4b5f3a4 100644 --- a/web/public/locales/zh-CN/views/live.json +++ b/web/public/locales/zh-CN/views/live.json @@ -87,7 +87,7 @@ }, "manualRecording": { "title": "按需录制", - "tips": "根据此摄像头的录制保留设置,手动启动事件。", + "tips": "根据此摄像头的录像存储设置,可以下载即时快照或手动触发事件记录。", "playInBackground": { "label": "后台播放", "desc": "启用此选项可在播放器隐藏时继续视频流播放。" @@ -134,6 +134,9 @@ "playInBackground": { "label": "后台播放", "tips": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "debug": { + "picker": "调试模式下无法切换视频流。调试将始终使用检测(detect)功能的视频流。" } }, "cameraSettings": { @@ -167,5 +170,16 @@ "transcription": { "enable": "启用实时音频转录", "disable": "关闭实时音频转录" + }, + "noCameras": { + "title": "未设置摄像头", + "description": "准备开始连接摄像头至 Frigate 。", + "buttonText": "添加摄像头" + }, + "snapshot": { + "takeSnapshot": "下载即时快照", + "noVideoSource": "当前无可用于快照的视频源。", + "captureFailed": "捕获快照失败。", + "downloadStarted": "快照下载已开始。" } } diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index 6cf6baad9..a12350c14 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -10,7 +10,9 @@ "general": "常规设置 - Frigate", "frigatePlus": "Frigate+ 设置 - Frigate", "notifications": "通知设置 - Frigate", - "enrichments": "增强功能设置 - Frigate" + "enrichments": "增强功能设置 - Frigate", + "cameraManagement": "管理摄像头 - Frigate", + "cameraReview": "摄像头核查设置 - Frigate" }, "menu": { "ui": "界面设置", @@ -23,7 +25,10 @@ "notifications": "通知", "frigateplus": "Frigate+", "enrichments": "增强功能", - "triggers": "触发器" + "triggers": "触发器", + "roles": "权限组", + "cameraManagement": "管理", + "cameraReview": "核查" }, "dialog": { "unsavedChanges": { @@ -46,6 +51,10 @@ "playAlertVideos": { "label": "播放警报视频", "desc": "默认情况下,实时监控页面上的最新警报会以一小段循环视频的形式进行播放。禁用此选项将仅显示浏览器本地缓存的静态图片。" + }, + "displayCameraNames": { + "label": "始终显示摄像头名称", + "desc": "在有多摄像头情况下的实时监控页面,将始终显示摄像头名称标签。" } }, "storedLayouts": { @@ -238,7 +247,8 @@ "mustNotBeSameWithCamera": "区域名称不能与摄像头名称相同。", "alreadyExists": "该摄像头已有相同的区域名称。", "mustNotContainPeriod": "区域名称不能包含句点。", - "hasIllegalCharacter": "区域名称包含非法字符。" + "hasIllegalCharacter": "区域名称包含非法字符。", + "mustHaveAtLeastOneLetter": "区域名称必须至少包含一个字母。" } }, "distance": { @@ -295,7 +305,7 @@ "name": { "title": "区域名称", "inputPlaceHolder": "请输入名称…", - "tips": "名称至少包含两个字符,且不能和摄像头或其他区域同名。
    当前仅支持英文与数字组合。" + "tips": "名称至少包含两个字符,其中至少需要一个英文字母,且不能和摄像头或其他区域同名。同时,当前仅支持英文与数字组合。" }, "inertia": { "title": "惯性", @@ -734,7 +744,7 @@ "triggers": { "documentTitle": "触发器", "management": { - "title": "触发器管理", + "title": "触发器", "desc": "管理 {{camera}} 的触发器。您可以使用“缩略图”类型,基于与所选追踪对象相似的缩略图来触发;也可以使用“描述”类型,基于与您指定的文本相似的描述来触发。" }, "addTrigger": "添加触发器", @@ -755,7 +765,9 @@ }, "actions": { "alert": "标记为警报", - "notification": "发送通知" + "notification": "发送通知", + "sub_label": "添加子标签", + "attribute": "添加属性" }, "dialog": { "createTrigger": { @@ -773,25 +785,28 @@ "form": { "name": { "title": "名称", - "placeholder": "输入触发器名称", + "placeholder": "触发器名称", "error": { - "minLength": "名称至少要两个字符。", - "invalidCharacters": "名称只能包含字母、数字、下划线和连字符。", + "minLength": "该字段至少需要两个字符。", + "invalidCharacters": "该字段只能包含字母、数字、下划线和连字符。", "alreadyExists": "此摄像头已存在同名触发器。" - } + }, + "description": "请输入用于识别此触发器的唯一名称或描述" }, "enabled": { "description": "开启/关闭此触发器" }, "type": { "title": "类型", - "placeholder": "选择触发类型" + "placeholder": "选择触发类型", + "description": "当检测到相似的追踪目标描述时触发", + "thumbnail": "当检测到相似的追踪目标缩略图时触发" }, "content": { "title": "内容", "imagePlaceholder": "选择图片", "textPlaceholder": "输入文字内容", - "imageDesc": "选择一张图片,当检测到相似图片时触发此操作。", + "imageDesc": "仅显示最近的 100 张缩略图。如果找不到需要的图片,请前往“浏览”页面查看更早的目标,并从菜单中设置触发器。", "textDesc": "输入文本,当检测到相似的追踪对象描述时触发此操作。", "error": { "required": "内容为必填项。" @@ -802,14 +817,20 @@ "error": { "min": "阈值必须大于 0", "max": "阈值必须小于 1" - } + }, + "desc": "设置此触发器的相似度阈值。阈值越高,触发所需的匹配就越精确。" }, "actions": { "title": "动作", - "desc": "默认情况下,Frigate 会为所有触发器发送 MQTT 消息。请选择此触发器触发时需要执行的附加操作。", + "desc": "默认情况下,Frigate 会为所有触发器发送 MQTT 消息。子标签会将触发器名称添加到对象标签中。属性是可搜索的元数据,独立存储在追踪对象的元数据中。", "error": { "min": "必须至少选择一项动作。" } + }, + "friendly_name": { + "title": "友好名称", + "placeholder": "为此触发器命名或添加描述", + "description": "(可选)为触发器添加友好名称或描述。" } } }, @@ -824,6 +845,27 @@ "updateTriggerFailed": "更新触发器失败:{{errorMessage}}", "deleteTriggerFailed": "删除触发器失败:{{errorMessage}}" } + }, + "semanticSearch": { + "title": "语义搜索已关闭", + "desc": "必须启用语义搜索功能才能使用触发器。" + }, + "wizard": { + "title": "创建触发器", + "step1": { + "description": "配置触发器的基础设置。" + }, + "step2": { + "description": "设置触发此操作的内容。" + }, + "step3": { + "description": "配置此触发器的相似度阈值与执行动作。" + }, + "steps": { + "nameAndType": "名称与类型", + "configureData": "配置数据", + "thresholdAndActions": "阈值与动作" + } } }, "roles": { @@ -845,7 +887,7 @@ "createRole": "权限组 {{role}} 创建成功", "updateCameras": "已更新摄像头至 {{role}} 权限组", "deleteRole": "已删除 {{role}} 权限组", - "userRolesUpdated": "已将分配到此权限组的 {{count}} 位用户更新为 “成员”,该权限组可访问所有摄像头。" + "userRolesUpdated_other": "已将分配到此权限组的 {{count}} 位用户更新为 “成员”,该权限组可访问所有摄像头。" }, "error": { "createRoleFailed": "创建权限组失败:{{errorMessage}}", @@ -879,9 +921,237 @@ "roleExists": "该权限组名称已存在。" }, "cameras": { - "title": "摄像头" + "title": "摄像头", + "desc": "请选择该权限组能够访问的摄像头。至少需要选择一个摄像头。", + "required": "至少要选择一个摄像头。" } } } + }, + "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": "例如:大门,后院等", + "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": "正在获取摄像头快照……" + } + }, + "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的转流功能来减少摄像头连接数。" + } + }, + "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 模块,这可能会提高与某些摄像头视频流的兼容性。" + } + }, + "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 27d0c91aa..5bd877736 100644 --- a/web/public/locales/zh-CN/views/system.json +++ b/web/public/locales/zh-CN/views/system.json @@ -42,7 +42,8 @@ "inferenceSpeed": "检测器推理速度", "cpuUsage": "检测器CPU使用率", "memoryUsage": "检测器内存使用率", - "temperature": "检测器温度" + "temperature": "检测器温度", + "cpuUsageInformation": "用于准备输入和输出数据的 CPU 资源,这些数据是供检测模型使用或由检测模型产生的。该数值并不衡量推理过程中的 CPU 使用情况,即使使用了 GPU 或加速器也是如此。" }, "hardwareInfo": { "title": "硬件信息", @@ -162,7 +163,8 @@ "reindexingEmbeddings": "正在重新索引嵌入(已完成 {{processed}}%)", "detectIsSlow": "{{detect}} 运行缓慢({{speed}}毫秒)", "detectIsVerySlow": "{{detect}} 运行非常缓慢({{speed}}毫秒)", - "cameraIsOffline": "{{camera}} 已离线" + "cameraIsOffline": "{{camera}} 已离线", + "shmTooLow": "/dev/shm 的分配空间过低(当前 {{total}} MB),应至少增加到 {{min}} MB。" }, "enrichments": { "title": "增强功能", 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..1371f9212 --- /dev/null +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -0,0 +1,8 @@ +{ + "toast": { + "success": { + "deletedImage": "已刪除的圖片", + "deletedModel_other": "成功刪除 {{count}} 個模型" + } + } +} diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json index 8571ea39f..3a22512af 100644 --- a/web/public/locales/zh-Hant/views/events.json +++ b/web/public/locales/zh-Hant/views/events.json @@ -36,5 +36,7 @@ "camera": "鏡頭", "detected": "已偵測", "suspiciousActivity": "可疑的活動", - "threateningActivity": "有威脅性的活動" + "threateningActivity": "有威脅性的活動", + "zoomIn": "放大", + "zoomOut": "縮小" } diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json index 5574bf0f0..90e8c3f63 100644 --- a/web/public/locales/zh-Hant/views/settings.json +++ b/web/public/locales/zh-Hant/views/settings.json @@ -9,7 +9,9 @@ "notifications": "通知設定 - Frigate", "masksAndZones": "遮罩與區域編輯器 - Frigate", "motionTuner": "移動偵測調教器 - Frigate", - "object": "除錯 - Frigate" + "object": "除錯 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "相機預覽設置 - Frigate" }, "menu": { "ui": "使用者介面", @@ -21,7 +23,9 @@ "users": "使用者", "notifications": "通知", "frigateplus": "Frigate+", - "triggers": "觸發" + "triggers": "觸發", + "cameraManagement": "管理", + "cameraReview": "預覽" }, "dialog": { "unsavedChanges": { @@ -85,6 +89,41 @@ }, "enrichments": { "title": "強化設定", - "unsavedChanges": "尚未儲存的強化設定變更" + "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": "相機品牌" + } } } 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/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/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/CameraNameLabel.tsx b/web/src/components/camera/FriendlyNameLabel.tsx similarity index 53% rename from web/src/components/camera/CameraNameLabel.tsx rename to web/src/components/camera/FriendlyNameLabel.tsx index ab022f5c8..ca0978852 100644 --- a/web/src/components/camera/CameraNameLabel.tsx +++ b/web/src/components/camera/FriendlyNameLabel.tsx @@ -2,12 +2,19 @@ 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 @@ -21,4 +28,17 @@ const CameraNameLabel = React.forwardRef< }); CameraNameLabel.displayName = LabelPrimitive.Root.displayName; -export { CameraNameLabel }; +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/ClassificationCard.tsx b/web/src/components/card/ClassificationCard.tsx index 5153b6d71..0e1138feb 100644 --- a/web/src/components/card/ClassificationCard.tsx +++ b/web/src/components/card/ClassificationCard.tsx @@ -6,15 +6,33 @@ import { ClassificationThreshold, } from "@/types/classification"; import { Event } from "@/types/event"; -import { useMemo, useRef, useState } from "react"; -import { isDesktop, isMobile } from "react-device-detect"; +import { forwardRef, useMemo, useRef, useState } from "react"; +import { isDesktop, isMobile, isMobileOnly } from "react-device-detect"; import { useTranslation } from "react-i18next"; import TimeAgo from "../dynamic/TimeAgo"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import { LuSearch } from "react-icons/lu"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { LuSearch, LuInfo } from "react-icons/lu"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { useNavigate } from "react-router-dom"; -import { getTranslatedLabel } from "@/utils/i18n"; +import { HiSquare2Stack } from "react-icons/hi2"; +import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, + MobilePageTrigger, +} from "../mobile/MobilePage"; type ClassificationCardProps = { className?: string; @@ -24,20 +42,28 @@ type ClassificationCardProps = { selected: boolean; i18nLibrary: string; showArea?: boolean; + count?: number; onClick: (data: ClassificationItemData, meta: boolean) => void; children?: React.ReactNode; }; -export function ClassificationCard({ - className, - imgClassName, - data, - threshold, - selected, - i18nLibrary, - showArea = true, - onClick, - children, -}: ClassificationCardProps) { +export const ClassificationCard = forwardRef< + HTMLDivElement, + ClassificationCardProps +>(function ClassificationCard( + { + className, + imgClassName, + data, + threshold, + selected, + i18nLibrary, + showArea = true, + count, + onClick, + children, + }, + ref, +) { const { t } = useTranslation([i18nLibrary]); const [imageLoaded, setImageLoaded] = useState(false); @@ -72,61 +98,82 @@ export function ClassificationCard({ }, [showArea, imageLoaded]); return ( - <> -
    { + const isMeta = e.metaKey || e.ctrlKey; + if (isMeta) { + e.stopPropagation(); + } + onClick(data, isMeta); + }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onClick(data, true); + }} + > + -
    - setImageLoaded(true)} - className={cn("size-44", imgClassName, isMobile && "w-full")} - src={`${baseUrl}${data.filepath}`} - onClick={(e) => { - e.stopPropagation(); - onClick(data, e.metaKey || e.ctrlKey); - }} - /> - {imageArea != undefined && ( -
    - {t("information.pixels", { ns: "common", area: imageArea })} + loading="lazy" + onLoad={() => setImageLoaded(true)} + src={`${baseUrl}${data.filepath}`} + /> + + {count && ( +
    +
    {count}
    {" "} + +
    + )} + {!count && imageArea != undefined && ( +
    + {t("information.pixels", { ns: "common", area: imageArea })} +
    + )} +
    +
    +
    +
    + {data.name == "unknown" ? t("details.unknown") : data.name} +
    + {data.score != undefined && ( +
    + {Math.round(data.score * 100)}%
    )}
    -
    -
    -
    -
    - {data.name == "unknown" ? t("details.unknown") : data.name} -
    - {data.score && ( -
    - {Math.round(data.score * 100)}% -
    - )} -
    -
    - {children} -
    -
    +
    + {children}
    - +
    ); -} +}); type GroupedClassificationCardProps = { group: ClassificationItemData[]; @@ -135,8 +182,8 @@ type GroupedClassificationCardProps = { selectedItems: string[]; i18nLibrary: string; objectType: string; + noClassificationLabel?: string; onClick: (data: ClassificationItemData | undefined) => void; - onSelectEvent: (event: Event) => void; children?: (data: ClassificationItemData) => React.ReactNode; }; export function GroupedClassificationCard({ @@ -145,20 +192,59 @@ export function GroupedClassificationCard({ threshold, selectedItems, i18nLibrary, - objectType, + noClassificationLabel = "details.none", onClick, - onSelectEvent, children, }: GroupedClassificationCardProps) { const navigate = useNavigate(); const { t } = useTranslation(["views/explore", i18nLibrary]); + const [detailOpen, setDetailOpen] = useState(false); // data - const allItemsSelected = useMemo( - () => group.every((data) => selectedItems.includes(data.filename)), - [group, selectedItems], - ); + const bestItem = useMemo(() => { + let best: undefined | ClassificationItemData = undefined; + + group.forEach((item) => { + if (item?.name != undefined && item.name != "none") { + if ( + best?.score == undefined || + (item.score && best.score < item.score) + ) { + best = item; + } + } + }); + + if (!best) { + return group.at(-1); + } + + const bestTyped: ClassificationItemData = best; + return { + ...bestTyped, + name: event + ? event.sub_label && event.sub_label !== "none" + ? event.sub_label + : t(noClassificationLabel) + : bestTyped.name, + score: event?.data?.sub_label_score, + }; + }, [group, event, noClassificationLabel, t]); + + const bestScoreStatus = useMemo(() => { + if (!bestItem?.score || !threshold) { + return "unknown"; + } + + if (bestItem.score >= threshold.recognition) { + return "match"; + } else if (bestItem.score >= threshold.unknown) { + return "potential"; + } else { + return "unknown"; + } + }, [bestItem, threshold]); const time = useMemo(() => { const item = group[0]; @@ -170,94 +256,158 @@ export function GroupedClassificationCard({ return item.timestamp * 1000; }, [group]); - return ( -
    { - if (selectedItems.length) { - onClick(undefined); - } - }} - onContextMenu={(e) => { - e.stopPropagation(); - e.preventDefault(); - onClick(undefined); - }} - > -
    -
    -
    - {getTranslatedLabel(objectType)} - {event?.sub_label - ? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)` - : ": " + t("details.unknown")} -
    - {time && ( - - )} -
    - {event && ( - - -
    { - navigate(`/explore?event_id=${event.id}`); - }} - > - -
    -
    - - - {t("details.item.button.viewInExplore", { - ns: "views/explore", - })} - - -
    - )} -
    + if (!bestItem) { + return null; + } -
    + { + if (meta || selectedItems.length > 0) { + onClick(undefined); + } else { + setDetailOpen(true); + } + }} + /> + { + if (!open) { + setDetailOpen(false); + } + }} > - {group.map((data: ClassificationItemData) => ( - { - if (meta || selectedItems.length > 0) { - onClick(data); - } else if (event) { - onSelectEvent(event); - } - }} - > - {children?.(data)} - - ))} -
    -
    + + e.preventDefault()} + > + <> +
    +
    + + {event?.sub_label && event.sub_label !== "none" + ? event.sub_label + : t(noClassificationLabel)} + {event?.sub_label && event.sub_label !== "none" && ( +
    +
    {`${Math.round((event.data.sub_label_score || 0) * 100)}%`}
    + + + + + + {t("details.scoreInfo", { ns: i18nLibrary })} + + +
    + )} +
    + + {time && ( + + )} + +
    + {isDesktop && ( +
    + {event && ( + + +
    { + navigate(`/explore?event_id=${event.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 index 671262994..de934482f 100644 --- a/web/src/components/card/EmptyCard.tsx +++ b/web/src/components/card/EmptyCard.tsx @@ -1,27 +1,30 @@ import React from "react"; import { Button } from "../ui/button"; import Heading from "../ui/heading"; +import { Link } from "react-router-dom"; type EmptyCardProps = { icon: React.ReactNode; title: string; description: string; buttonText?: string; + link?: string; }; export function EmptyCard({ icon, title, description, buttonText, + link, }: EmptyCardProps) { return (
    {icon} {title} -
    {description}
    +
    {description}
    {buttonText?.length && ( )}
    diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index cf0685caa..d95c6f318 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,9 @@ 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"; type ExportProps = { className: string; @@ -145,7 +147,7 @@ export default function ExportCard({ <> {exportedRecording.thumb_path.length > 0 ? ( setLoading(false)} /> @@ -155,56 +157,77 @@ 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")} + + + + )} + {!exportedRecording.in_progress && ( + + + + setEditName({ + original: exportedRecording.name, + update: undefined, + }) + } + > + + + + {t("tooltip.editName")} + + )} + + + + onDelete({ + file: exportedRecording.id, + exportName: exportedRecording.name, + }) + } + > + + + + {t("tooltip.deleteExport")} + +
    {!exportedRecording.in_progress && ( @@ -219,15 +242,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 b1e7a7b57..8fc4024db 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -34,9 +34,11 @@ 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"; type ReviewCardProps = { event: ReviewSegment; @@ -83,6 +85,11 @@ export default function ReviewCard({ if (response.status == 200) { toast.success(t("export.toast.success"), { position: "top-center", + action: ( + + + + ), }); } }) @@ -137,7 +144,7 @@ export default function ReviewCard({ className={cn( "size-full rounded-lg", activeReviewItem?.id == event.id && - "outline outline-[3px] outline-offset-1 outline-selected", + "outline outline-[3px] -outline-offset-[2.8px] outline-selected duration-200", imgLoaded ? "visible" : "invisible", )} src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`} @@ -158,21 +165,33 @@ 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, "size-3 text-white")} +
    + ))} + {event.data.audio.map((audio, idx) => ( +
    + {getIconForLabel(audio, "size-3 text-white")} +
    + ))} +
    {formattedDate}
    @@ -199,6 +218,14 @@ 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 8ec922ae2..0b82475c8 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -150,7 +150,9 @@ export default function SearchThumbnail({ .filter( (item) => item !== undefined && !item.includes("-verified"), ) - .map((text) => getTranslatedLabel(text, searchResult.data.type)) + .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 e23d1c3f6..808ad2831 100644 --- a/web/src/components/card/SearchThumbnailFooter.tsx +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -13,8 +13,7 @@ type SearchThumbnailProps = { columns: number; findSimilar: () => void; refreshResults: () => void; - showObjectLifecycle: () => void; - showSnapshot: () => void; + showTrackingDetails: () => void; addTrigger: () => void; }; @@ -23,8 +22,7 @@ export default function SearchThumbnailFooter({ columns, findSimilar, refreshResults, - showObjectLifecycle, - showSnapshot, + showTrackingDetails, addTrigger, }: SearchThumbnailProps) { const { t } = useTranslation(["views/search"]); @@ -42,11 +40,11 @@ export default function SearchThumbnailFooter({ return (
    4 && "items-start sm:flex-col lg:flex-row lg:items-center", )} > -
    +
    {searchResult.end_time ? ( ) : ( @@ -61,8 +59,7 @@ 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..c47765d76 --- /dev/null +++ b/web/src/components/classification/ClassificationModelEditDialog.tsx @@ -0,0 +1,481 @@ +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 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 { + // State model - update classes + // Note: For state models, updating classes requires renaming categories + // which is handled through the dataset API, not the config API + // We'll need to implement this by calling the rename endpoint for each class + // For now, we just show a message that this requires retraining + + toast.info(t("edit.stateClassesInfo"), { + position: "top-center", + }); + } + + onSuccess(); + onClose(); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + }; + const errorMessage = + 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, 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..06bf1f850 --- /dev/null +++ b/web/src/components/classification/ClassificationModelWizardDialog.tsx @@ -0,0 +1,218 @@ +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(); + }; + + 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..8a510d33d --- /dev/null +++ b/web/src/components/classification/wizard/Step1NameAndDefine.tsx @@ -0,0 +1,500 @@ +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()) + .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..e4c157526 --- /dev/null +++ b/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -0,0 +1,508 @@ +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 { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; + +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 { 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 3: Kick off training + await axios.post(`/classification/${step1Data.modelName}/train`); + + toast.success(t("wizard.step3.trainingStarted")); + setIsTraining(true); + }, + [step1Data, step2Data, t], + ); + + const handleContinueClassification = useCallback(async () => { + // Mark selected images with current class + const newClassifications = { ...imageClassifications }; + 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")); + + 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]); + + // For state models on the last class, require all images to be classified + const isLastClass = currentClassIndex === allClasses.length - 1; + const canProceed = useMemo(() => { + if (step1Data.modelType === "state" && isLastClass) { + // Check if all 24 images will be classified after current selections are applied + const totalImages = unknownImages.slice(0, 24).length; + + // Count images that will be classified (either already classified or currently selected) + const allImages = unknownImages.slice(0, 24); + const willBeClassified = allImages.filter((img) => { + return imageClassifications[img] || selectedImages.has(img); + }).length; + + return willBeClassified >= totalImages; + } + return true; + }, [ + step1Data.modelType, + isLastClass, + unknownImages, + imageClassifications, + selectedImages, + ]); + + 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 ? ( +
    + {!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)} + > + {`Example +
    + ); + })} +
    + )} +
    +
    + ) : ( +
    +

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

    + +
    + )} + + {!isTraining && ( +
    + + + + + + {!canProceed && ( + + + {t("wizard.step3.allImagesRequired", { + count: unclassifiedImages.length, + })} + + + )} + +
    + )} +
    + ); +} diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index cd0b118c9..c772bc2ba 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -9,7 +9,7 @@ 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 { @@ -76,7 +76,7 @@ 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/CameraNameLabel"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useIsCustomRole } from "@/hooks/use-is-custom-role"; @@ -87,6 +87,8 @@ type CameraGroupSelectorProps = { export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { const { t } = useTranslation(["components/camera"]); const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); + const isCustomRole = useIsCustomRole(); // tooltip @@ -119,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 (isCustomRole) { + 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, isCustomRole]); // add group @@ -139,6 +153,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { activeGroup={group} setGroup={setGroup} deleteGroup={deleteGroup} + isCustomRole={isCustomRole} />
    setAddGroup(true)} - > - - + {!isCustomRole && ( + + )} {isMobile && }
    @@ -228,6 +245,7 @@ type NewGroupDialogProps = { activeGroup?: string; setGroup: (value: string | undefined, replace?: boolean | undefined) => void; deleteGroup: () => void; + isCustomRole?: boolean; }; function NewGroupDialog({ open, @@ -236,6 +254,7 @@ function NewGroupDialog({ activeGroup, setGroup, deleteGroup, + isCustomRole, }: NewGroupDialogProps) { const { t } = useTranslation(["components/camera"]); const { mutate: updateConfig } = useSWR("config"); @@ -261,6 +280,12 @@ function NewGroupDialog({ `${activeGroup}-draggable-layout`, ); + useEffect(() => { + if (!open) { + setEditState("none"); + } + }, [open]); + // callbacks const onDeleteGroup = useCallback( @@ -349,13 +374,7 @@ function NewGroupDialog({ position="top-center" closeButton={true} /> - { - setEditState("none"); - setOpen(open); - }} - > + {t("group.label")} {t("group.edit")} -
    - -
    + +
    + )}
    {currentGroups.map((group) => ( @@ -401,6 +422,7 @@ function NewGroupDialog({ group={group} onDeleteGroup={() => onDeleteGroup(group[0])} onEditGroup={() => onEditGroup(group)} + isReadOnly={isCustomRole} /> ))}
    @@ -512,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); @@ -564,7 +588,7 @@ export function CameraGroupRow({ - {isMobile && ( + {isMobile && !isReadOnly && ( <> @@ -589,7 +613,7 @@ export function CameraGroupRow({ )} - {!isMobile && ( + {!isMobile && !isReadOnly && (
    diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index 93b8a8651..baeccf06f 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -190,7 +190,7 @@ export function CamerasFilterContent({ key={item} isChecked={currentCameras?.includes(item) ?? false} label={item} - isCameraName={true} + type={"camera"} disabled={ mainCamera !== undefined && currentCameras !== undefined && diff --git a/web/src/components/filter/FilterSwitch.tsx b/web/src/components/filter/FilterSwitch.tsx index fa8709d96..d282b9e12 100644 --- a/web/src/components/filter/FilterSwitch.tsx +++ b/web/src/components/filter/FilterSwitch.tsx @@ -1,29 +1,39 @@ import { Switch } from "../ui/switch"; import { Label } from "../ui/label"; -import { CameraNameLabel } from "../camera/CameraNameLabel"; +import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel"; type FilterSwitchProps = { label: string; disabled?: boolean; isChecked: boolean; isCameraName?: boolean; + type?: string; + extraValue?: string; onCheckedChange: (checked: boolean) => void; }; export default function FilterSwitch({ label, disabled = false, isChecked, - isCameraName = false, + type = "", + extraValue = "", onCheckedChange, }: FilterSwitchProps) { return (
    - {isCameraName ? ( + {type === "camera" ? ( + ) : type === "zone" ? ( + ) : (