diff --git a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml index 21e4f746f..a32ee5938 100644 --- a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml +++ b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -6,7 +6,7 @@ body: value: | Use this form to submit a reproducible bug in Frigate or Frigate's UI. - Before submitting your bug report, please [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. + Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. **If you are unsure if your issue is actually a bug or not, please submit a support request first.** @@ -14,6 +14,7 @@ body: [prs]: https://www.github.com/blakeblackshear/frigate/pulls [docs]: https://docs.frigate.video [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + [ai]: https://docs.frigate.video - type: checkboxes attributes: label: Checklist @@ -26,6 +27,8 @@ body: - label: I have tried a different browser to see if it is related to my browser. required: true - label: I have tried reproducing the issue in [incognito mode](https://www.computerworld.com/article/1719851/how-to-go-incognito-in-chrome-firefox-safari-and-edge.html) to rule out problems with any third party extensions or plugins I have installed. + - label: I have asked the AI at https://docs.frigate.video about my issue. + required: true - type: textarea id: description attributes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44f472beb..65ebaa408 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: name: AMD64 Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -47,7 +47,7 @@ jobs: name: ARM Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -77,42 +77,12 @@ jobs: rpi.tags=${{ steps.setup.outputs.image-name }}-rpi *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max - jetson_jp5_build: - if: false - runs-on: ubuntu-22.04 - name: Jetson Jetpack 5 - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Set up QEMU and Buildx - id: setup - uses: ./.github/actions/setup - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push TensorRT (Jetson, Jetpack 5) - env: - ARCH: arm64 - BASE_IMAGE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - SLIM_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - TRT_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime - uses: docker/bake-action@v6 - with: - source: . - push: true - targets: tensorrt - files: docker/tensorrt/trt.hcl - set: | - tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp5 - *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp5 - *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp5,mode=max jetson_jp6_build: runs-on: ubuntu-22.04-arm name: Jetson Jetpack 6 steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -143,7 +113,7 @@ jobs: - amd64_build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU and Buildx @@ -185,7 +155,7 @@ jobs: - arm64_build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU and Buildx diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ebe107d3d..a96df2276 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,7 +19,7 @@ jobs: env: DOCKER_BUILDKIT: "1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - uses: actions/setup-node@master @@ -40,7 +40,7 @@ jobs: name: Web - Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - uses: actions/setup-node@master @@ -56,7 +56,7 @@ jobs: name: Web - Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - uses: actions/setup-node@master @@ -76,7 +76,7 @@ jobs: name: Python Checks steps: - name: Check out the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up Python ${{ env.DEFAULT_PYTHON }} @@ -99,7 +99,7 @@ jobs: name: Python Tests steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Set up QEMU diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c19ca6cb..166415053 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - id: lowercaseRepo diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index ca93f4a3b..67b1cbc07 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -212,6 +212,7 @@ COPY docker/main/rootfs/ / # Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc) FROM slim-base AS deps ARG TARGETARCH +ARG BASE_IMAGE ARG DEBIAN_FRONTEND # http://stackoverflow.com/questions/48162574/ddg#49462622 @@ -255,6 +256,10 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \ pip3 install -U /deps/wheels/*.whl +# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image) +RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \ + bash -c "bash /deps/install_memryx.sh" + COPY --from=deps-rootfs / / RUN ldconfig diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index bd9f363e9..ff3fb4d13 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -19,7 +19,8 @@ apt-get -qq install --no-install-recommends -y \ nethogs \ libgl1 \ libglib2.0-0 \ - libusb-1.0.0 + libusb-1.0.0 \ + libgomp1 # memryx detector update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 @@ -34,9 +35,13 @@ rm /tmp/libedgetpu1-max.deb # install mesa-teflon-delegate from bookworm-backports # Only available for arm64 at the moment if [[ "${TARGETARCH}" == "arm64" ]]; then - echo "deb http://deb.debian.org/debian bookworm-backports main" | tee /etc/apt/sources.list.d/bookworm-backports.list - apt-get -qq update - apt-get -qq install --no-install-recommends --no-install-suggests -y mesa-teflon-delegate/bookworm-backports + if [[ "${BASE_IMAGE}" == *"nvcr.io/nvidia/tensorrt"* ]]; then + echo "Info: Skipping apt-get commands because BASE_IMAGE includes 'nvcr.io/nvidia/tensorrt' for arm64." + else + echo "deb http://deb.debian.org/debian bookworm-backports main" | tee /etc/apt/sources.list.d/bookworm-backbacks.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y mesa-teflon-delegate/bookworm-backports + fi fi # ffmpeg -> amd64 diff --git a/docker/main/install_memryx.sh b/docker/main/install_memryx.sh new file mode 100644 index 000000000..f96181ae0 --- /dev/null +++ b/docker/main/install_memryx.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Download the MxAccl for Frigate github release +wget https://github.com/memryx/mx_accl_frigate/archive/refs/heads/main.zip -O /tmp/mxaccl.zip +unzip /tmp/mxaccl.zip -d /tmp +mv /tmp/mx_accl_frigate-main /opt/mx_accl_frigate +rm /tmp/mxaccl.zip + +# Install Python dependencies +pip3 install -r /opt/mx_accl_frigate/freeze + +# Link the Python package dynamically +SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])") +ln -s /opt/mx_accl_frigate/memryx "$SITE_PACKAGES/memryx" + +# Copy architecture-specific shared libraries +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + cp /opt/mx_accl_frigate/memryx/x86/libmemx.so* /usr/lib/x86_64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/x86/libmx_accl.so* /usr/lib/x86_64-linux-gnu/ +elif [[ "$ARCH" == "aarch64" ]]; then + cp /opt/mx_accl_frigate/memryx/arm/libmemx.so* /usr/lib/aarch64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/arm/libmx_accl.so* /usr/lib/aarch64-linux-gnu/ +else + echo "Unsupported architecture: $ARCH" + exit 1 +fi + +# Refresh linker cache +ldconfig diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 7a2e2d6df..e5f5e6eec 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -1,22 +1,23 @@ aiofiles == 24.1.* click == 8.1.* # FastAPI -aiohttp == 3.11.3 -starlette == 0.41.2 -starlette-context == 0.3.6 -fastapi == 0.115.* -uvicorn == 0.30.* +aiohttp == 3.12.* +starlette == 0.47.* +starlette-context == 0.4.* +fastapi[standard-no-fastapi-cloud-cli] == 0.116.* +uvicorn == 0.35.* slowapi == 0.1.* -joserfc == 1.0.* -pathvalidate == 3.2.* +joserfc == 1.2.* +cryptography == 44.0.* +pathvalidate == 3.3.* markupsafe == 3.0.* -python-multipart == 0.0.12 +python-multipart == 0.0.20 # Classification Model Training tensorflow == 2.19.* ; platform_machine == 'aarch64' tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64' # General mypy == 1.6.1 -onvif-zeep-async == 3.1.* +onvif-zeep-async == 4.0.* paho-mqtt == 2.1.* pandas == 2.2.* peewee == 3.17.* @@ -30,7 +31,7 @@ ruamel.yaml == 0.18.* tzlocal == 5.2 requests == 2.32.* types-requests == 2.32.* -norfair == 2.2.* +norfair == 2.3.* setproctitle == 1.3.* ws4py == 0.5.* unidecode == 1.3.* diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run index af3bc04de..4ce1c133f 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run @@ -10,7 +10,7 @@ echo "[INFO] Starting certsync..." lefile="/etc/letsencrypt/live/frigate/fullchain.pem" -tls_enabled=`python3 /usr/local/nginx/get_tls_settings.py | jq -r .enabled` +tls_enabled=`python3 /usr/local/nginx/get_listen_settings.py | jq -r .tls.enabled` while true do diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run index 273182930..8bd9b5250 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -85,7 +85,7 @@ python3 /usr/local/nginx/get_base_path.py | \ -out /usr/local/nginx/conf/base_path.conf # build templates for optional TLS support -python3 /usr/local/nginx/get_tls_settings.py | \ +python3 /usr/local/nginx/get_listen_settings.py | \ tempio -template /usr/local/nginx/templates/listen.gotmpl \ -out /usr/local/nginx/conf/listen.conf diff --git a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py b/docker/main/rootfs/usr/local/nginx/get_listen_settings.py similarity index 71% rename from docker/main/rootfs/usr/local/nginx/get_tls_settings.py rename to docker/main/rootfs/usr/local/nginx/get_listen_settings.py index d2e704056..d879db56e 100644 --- a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py +++ b/docker/main/rootfs/usr/local/nginx/get_listen_settings.py @@ -26,6 +26,10 @@ try: except FileNotFoundError: config: dict[str, Any] = {} -tls_config: dict[str, Any] = config.get("tls", {"enabled": True}) +tls_config: dict[str, any] = config.get("tls", {"enabled": True}) +networking_config = config.get("networking", {}) +ipv6_config = networking_config.get("ipv6", {"enabled": False}) -print(json.dumps(tls_config)) +output = {"tls": tls_config, "ipv6": ipv6_config} + +print(json.dumps(output)) diff --git a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl index 093d5f68e..066f872cb 100644 --- a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl +++ b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl @@ -1,33 +1,45 @@ -# intended for internal traffic, not protected by auth + +# Internal (IPv4 always; IPv6 optional) listen 5000; +{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:5000;{{ end }}{{ end }} + -{{ if not .enabled }} # intended for external traffic, protected by auth -listen 8971; +{{ if .tls }} + {{ if .tls.enabled }} + # external HTTPS (IPv4 always; IPv6 optional) + listen 8971 ssl; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971 ssl;{{ end }}{{ end }} + + ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem; + + # generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP + # https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7 + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + # ACME challenge location + location /.well-known/acme-challenge/ { + default_type "text/plain"; + root /etc/letsencrypt/www; + } + {{ else }} + # external HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} + {{ end }} {{ else }} -# intended for external traffic, protected by auth -listen 8971 ssl; - -ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem; -ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem; - -# generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP -# https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7 -ssl_session_timeout 1d; -ssl_session_cache shared:MozSSL:10m; # about 40000 sessions -ssl_session_tickets off; - -# modern configuration -ssl_protocols TLSv1.3; -ssl_prefer_server_ciphers off; - -# HSTS (ngx_http_headers_module is required) (63072000 seconds) -add_header Strict-Transport-Security "max-age=63072000" always; - -# ACME challenge location -location /.well-known/acme-challenge/ { - default_type "text/plain"; - root /etc/letsencrypt/www; -} + # (No tls section) default to HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} {{ end }} diff --git a/docker/memryx/user_installation.sh b/docker/memryx/user_installation.sh new file mode 100644 index 000000000..20c9b8ece --- /dev/null +++ b/docker/memryx/user_installation.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e # Exit immediately if any command fails +set -o pipefail + +echo "Starting MemryX driver and runtime installation..." + +# Detect architecture +arch=$(uname -m) + +# Purge existing packages and repo +echo "Removing old MemryX installations..." +# Remove any holds on MemryX packages (if they exist) +sudo apt-mark unhold memx-* mxa-manager || true +sudo apt purge -y memx-* mxa-manager || true +sudo rm -f /etc/apt/sources.list.d/memryx.list /etc/apt/trusted.gpg.d/memryx.asc + +# Install kernel headers +echo "Installing kernel headers for: $(uname -r)" +sudo apt update +sudo apt install -y dkms linux-headers-$(uname -r) + +# Add MemryX key and repo +echo "Adding MemryX GPG key and repository..." +wget -qO- https://developer.memryx.com/deb/memryx.asc | sudo tee /etc/apt/trusted.gpg.d/memryx.asc >/dev/null +echo 'deb https://developer.memryx.com/deb stable main' | sudo tee /etc/apt/sources.list.d/memryx.list >/dev/null + +# Update and install memx-drivers +echo "Installing memx-drivers..." +sudo apt update +sudo apt install -y memx-drivers + +# ARM-specific board setup +if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then + echo "Running ARM board setup..." + sudo mx_arm_setup +fi + +echo -e "\n\n\033[1;31mYOU MUST RESTART YOUR COMPUTER NOW\033[0m\n\n" + +# 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 installation complete!" diff --git a/docker/rockchip/Dockerfile b/docker/rockchip/Dockerfile index 668250439..70309f02e 100644 --- a/docker/rockchip/Dockerfile +++ b/docker/rockchip/Dockerfile @@ -11,7 +11,8 @@ COPY docker/main/requirements-wheels.txt /requirements-wheels.txt COPY docker/rockchip/requirements-wheels-rk.txt /requirements-wheels-rk.txt RUN sed -i "/https:\/\//d" /requirements-wheels.txt RUN sed -i "/onnxruntime/d" /requirements-wheels.txt -RUN pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.txt +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.txt RUN rm -rf /rk-wheels/opencv_python-* RUN rm -rf /rk-wheels/torch-* diff --git a/docker/tensorrt/Dockerfile.amd64 b/docker/tensorrt/Dockerfile.amd64 index 906e113a8..cbbb28bb6 100644 --- a/docker/tensorrt/Dockerfile.amd64 +++ b/docker/tensorrt/Dockerfile.amd64 @@ -12,7 +12,10 @@ ARG PIP_BREAK_SYSTEM_PACKAGES # Install TensorRT wheels COPY docker/tensorrt/requirements-amd64.txt /requirements-tensorrt.txt COPY docker/main/requirements-wheels.txt /requirements-wheels.txt -RUN pip3 wheel --wheel-dir=/trt-wheels -c /requirements-wheels.txt -r /requirements-tensorrt.txt + +# remove dependencies from the requirements that have type constraints +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/trt-wheels -c /requirements-wheels.txt -r /requirements-tensorrt.txt FROM deps AS frigate-tensorrt ARG PIP_BREAK_SYSTEM_PACKAGES diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index 2f4d43a6a..71ad5c335 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -50,7 +50,7 @@ cameras: ### Configuring Minimum Volume -The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. MQTT explorer can be used on the audio topic to see what volume level is being detected. +The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. :::tip diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md index bf878d6bd..7e77229b6 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -59,6 +59,7 @@ The default session length for user authentication in Frigate is 24 hours. This While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds. The default value of `86400` will expire the authentication session after 24 hours. Some other examples: + - `0`: Setting the session length to 0 will require a user to log in every time they access the application or after a very short, immediate timeout. - `604800`: Setting the session length to 604800 will require a user to log in if the token is not refreshed for 7 days. @@ -133,6 +134,31 @@ proxy: default_role: viewer ``` +## Role mapping + +In some environments, upstream identity providers (OIDC, SAML, LDAP, etc.) do not pass a Frigate-compatible role directly, but instead pass one or more group claims. To handle this, Frigate supports a `role_map` that translates upstream group names into Frigate’s internal roles (`admin` or `viewer`). + +```yaml +proxy: + ... + header_map: + user: x-forwarded-user + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer +``` + +In this example: + +- If the proxy passes a role header containing `sysadmins` or `access-level-security`, the user is assigned the `admin` role. +- If the proxy passes a role header containing `camera-viewer`, the user is assigned the `viewer` role. +- If no mapping matches, Frigate falls back to `default_role` if configured. +- If `role_map` is not defined, Frigate assumes the role header directly contains `admin` or `viewer`. + #### Port Considerations **Authenticated Port (8971)** diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md index 3026615d4..d14946eaf 100644 --- a/docs/docs/configuration/face_recognition.md +++ b/docs/docs/configuration/face_recognition.md @@ -24,7 +24,7 @@ Frigate needs to first detect a `person` before it can detect and recognize a fa Frigate has support for two face recognition model types: - **small**: Frigate will run a FaceNet embedding model to recognize faces, which runs locally on the CPU. This model is optimized for efficiency and is not as accurate. -- **large**: Frigate will run a large ArcFace embedding model that is optimized for accuracy. It is only recommended to be run when an integrated or dedicated GPU is available. +- **large**: Frigate will run a large ArcFace embedding model that is optimized for accuracy. It is only recommended to be run when an integrated or dedicated GPU / NPU is available. In both cases, a lightweight face landmark detection model is also used to align faces before running recognition. @@ -34,7 +34,7 @@ All of these features run locally on your system. The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently. -The `large` model is optimized for accuracy, an integrated or discrete GPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. +The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. ## Configuration @@ -73,6 +73,9 @@ Fine-tune face recognition with these optional parameters at the global level of - Default: `100`. - `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this. - Default: `True`. +- `device`: Target a specific device to run the face recognition model on (multi-GPU installation). + - Default: `None`. + - Note: This setting is only applicable when using the `large` model. See [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/) ## Usage diff --git a/docs/docs/configuration/hardware_acceleration_enrichments.md b/docs/docs/configuration/hardware_acceleration_enrichments.md index 1f894d345..84688b8b4 100644 --- a/docs/docs/configuration/hardware_acceleration_enrichments.md +++ b/docs/docs/configuration/hardware_acceleration_enrichments.md @@ -5,11 +5,11 @@ title: Enrichments # Enrichments -Some of Frigate's enrichments can use a discrete GPU for accelerated processing. +Some of Frigate's enrichments can use a discrete GPU / NPU for accelerated processing. ## Requirements -Object detection and enrichments (like Semantic Search, Face Recognition, and License Plate Recognition) are independent features. To use a GPU for object detection, see the [Object Detectors](/configuration/object_detectors.md) documentation. If you want to use your GPU for any supported enrichments, you must choose the appropriate Frigate Docker image for your GPU and configure the enrichment according to its specific documentation. +Object detection and enrichments (like Semantic Search, Face Recognition, and License Plate Recognition) are independent features. To use a GPU / NPU for object detection, see the [Object Detectors](/configuration/object_detectors.md) documentation. If you want to use your GPU for any supported enrichments, you must choose the appropriate Frigate Docker image for your GPU / NPU and configure the enrichment according to its specific documentation. - **AMD** @@ -23,6 +23,9 @@ Object detection and enrichments (like Semantic Search, Face Recognition, and Li - Nvidia GPUs will automatically be detected and used for enrichments in the `-tensorrt` Frigate image. - Jetson devices will automatically be detected and used for enrichments in the `-tensorrt-jp6` Frigate image. +- **RockChip** + - RockChip NPU will automatically be detected and used for semantic search v1 and face recognition in the `-rk` Frigate image. + Utilizing a GPU for enrichments does not require you to use the same GPU for object detection. For example, you can run the `tensorrt` Docker image for enrichments and still use other dedicated hardware like a Coral or Hailo for object detection. However, one combination that is not supported is TensorRT for object detection and OpenVINO for enrichments. :::note diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index 933fd72d3..cbc3781f9 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -67,9 +67,9 @@ Fine-tune the LPR feature using these optional parameters at the global level of - **`min_area`**: Defines the minimum area (in pixels) a license plate must be before recognition runs. - Default: `1000` pixels. Note: this is intentionally set very low as it is an _area_ measurement (length x width). For reference, 1000 pixels represents a ~32x32 pixel square in your camera image. - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. -- **`device`**: Device to use to run license plate recognition models. +- **`device`**: Device to use to run license plate detection *and* recognition models. - Default: `CPU` - - This can be `CPU` or `GPU`. For users without a model that detects license plates natively, using a GPU may increase performance of the models, especially the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + - This can be `CPU` or one of [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/). For users without a model that detects license plates natively, using a GPU may increase performance of the models, especially the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. - **`model_size`**: The size of the model used to detect text on plates. - Default: `small` - This can be `small` or `large`. The `large` model uses an enhanced text detector and is more accurate at finding text on plates but slower than the `small` model. For most users, the small model is recommended. For users in countries with multiple lines of text on plates, the large model is recommended. Note that using the large model does not improve _text recognition_, but it may improve _text detection_. diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index e048e0ec5..fbbeb296e 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -13,12 +13,17 @@ 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. **AMD** - [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection. - [ONNX](#onnx): ROCm will automatically be detected and used as a detector in the `-rocm` Frigate image when a supported ONNX model is configured. +**Apple Silicon** + +- [Apple Silicon](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices. + **Intel** - [OpenVino](#openvino-detector): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. @@ -52,7 +57,7 @@ This does not affect using hardware for accelerating other tasks such as [semant # Officially Supported Detectors -Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `hailo8l`, `onnx`, `openvino`, `rknn`, and `tensorrt`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. +Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `hailo8l`, `memryx`, `onnx`, `openvino`, `rknn`, and `tensorrt`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. ## Edge TPU Detector @@ -240,6 +245,8 @@ 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"`. @@ -264,7 +271,7 @@ detectors: ::: -### Supported Models +### OpenVINO Supported Models #### SSDLite MobileNet v2 @@ -402,6 +409,59 @@ model: 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-arm64-standard`. + +### Setup + +1. Setup the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) and run the client +2. Configure the detector in Frigate and startup Frigate + +### Configuration + +Using the detector config below will connect to the client: + +```yaml +detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 +``` + +### Apple Silicon Supported Models + +There is no default model provided, the following formats are supported: + +#### YOLO (v3, v4, v7, v9) + +YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. + +:::tip + +The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. + +::: + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + ## AMD/ROCm GPU detector ### Setup @@ -483,7 +543,7 @@ We unset the `HSA_OVERRIDE_GFX_VERSION` to prevent an existing override from mes $ docker exec -it frigate /bin/bash -c '(unset HSA_OVERRIDE_GFX_VERSION && /opt/rocm/bin/rocminfo |grep gfx)' ``` -### Supported Models +### ROCm Supported Models See [ONNX supported models](#supported-models) for supported models, there are some caveats: @@ -526,7 +586,7 @@ detectors: ::: -### Supported Models +### ONNX Supported Models There is no default model provided, the following formats are supported: @@ -699,6 +759,196 @@ To verify that the integration is working correctly, start Frigate and observe t # Community Supported Detectors +## MemryX MX3 + +This detector is available for use with the MemryX MX3 accelerator M.2 module. Frigate supports the MX3 on compatible hardware platforms, providing efficient and high-performance object detection. + +See the [installation docs](../frigate/installation.md#memryx-mx3) for information on configuring the MemryX hardware. + +To configure a MemryX detector, simply set the `type` attribute to `memryx` and follow the configuration guide below. + +### Configuration + +To configure the MemryX detector, use the following example configuration: + +#### Single PCIe MemryX MX3 + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 +``` + +#### Multiple PCIe MemryX MX3 Modules + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + + memx1: + type: memryx + device: PCIe:1 + + memx2: + type: memryx + device: PCIe:2 +``` + +### Supported Models + +MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + +#### YOLO-NAS + +The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded from the [Models Section](#downloading-yolo-nas-model) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +**Note:** The default model for the MemryX detector is YOLO-NAS 320x320. + +The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. + +- The default size of **320x320** is optimized for lower CPU usage and faster inference times. + +##### Configuration + +Below is the recommended configuration for using the **YOLO-NAS** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolonas + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolonas.zip + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### YOLOv9 + +The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) like in the [Models Section](#yolov9-1) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +##### Configuration + +Below is the recommended configuration for using the **YOLOv9** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolo-generic + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolov9.zip + # The .zip file must contain: + # ├── yolov9.dfp (a file ending with .dfp) + # └── yolov9_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### YOLOX + +The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP. + +##### Configuration + +Below is the recommended configuration for using the **YOLOX** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolox + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float_denorm + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolox.zip + # The .zip file must contain: + # ├── yolox.dfp (a file ending with .dfp) +``` + +#### SSDLite MobileNet v2 + +The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP. + +##### Configuration + +Below is the recommended configuration for using the **SSDLite MobileNet v2** model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: ssd + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/ssdlite_mobilenet.zip + # The .zip file must contain: + # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) + # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### Using a Custom Model + +To use your own model: + +1. Package your compiled model into a `.zip` file. + +2. The `.zip` must contain the compiled `.dfp` file. + +3. Depending on the model, the compiler may also generate a cropped post-processing network. If present, it will be named with the suffix `_post.onnx`. + +4. Bind-mount the `.zip` file into the container and specify its path using `model.path` in your config. + +5. Update the `labelmap_path` to match your custom model's labels. + +For detailed instructions on compiling models, refer to the [MemryX Compiler](https://developer.memryx.com/tools/neural_compiler.html#usage) docs and [Tutorials](https://developer.memryx.com/tutorials/tutorials.html). + +```yaml + # The detector automatically selects the default model if nothing is provided in the config. + # + # Optionally, you can specify a local model path as a .zip file to override the default. + # If a local path is provided and the file exists, it will be used instead of downloading. + # + # Example: + # path: /config/yolonas.zip + # + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` +--- + ## NVidia TensorRT Detector Nvidia Jetson devices may be used for object detection using the TensorRT libraries. Due to the size of the additional libraries, this detector is only provided in images with the `-tensorrt-jp6` tag suffix, e.g. `ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6`. This detector is designed to work with Yolo models for object detection. @@ -824,7 +1074,7 @@ $ cat /sys/kernel/debug/rknpu/load ::: -### Supported Models +### RockChip Supported Models This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for two). Lines that are required at least to use the detector are labeled as required, all other lines are optional. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index e54f07a2b..7b324801b 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -73,6 +73,12 @@ tls: # Optional: Enable TLS for port 8971 (default: shown below) enabled: True +# Optional: IPv6 configuration +networking: + # Optional: Enable IPv6 on 5000, and 8971 if tls is configured (default: shown below) + ipv6: + enabled: False + # Optional: Proxy configuration proxy: # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth @@ -82,7 +88,13 @@ proxy: # See the docs for more info. header_map: user: x-forwarded-user - role: x-forwarded-role + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer # Optional: Url for logging out a user. This sets the location of the logout url in # the UI. logout_url: /api/logout @@ -586,6 +598,9 @@ semantic_search: # Optional: Set the model size used for embeddings. (default: shown below) # NOTE: small model runs on CPU and large model runs on GPU model_size: "small" + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None # Optional: Configuration for face recognition capability # NOTE: enabled, min_area can be overridden at the camera level @@ -609,6 +624,9 @@ face_recognition: blur_confidence_filter: True # Optional: Set the model size used face recognition. (default: shown below) model_size: small + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None # Optional: Configuration for license plate recognition capability # NOTE: enabled, min_area, and enhancement can be overridden at the camera level @@ -616,6 +634,7 @@ lpr: # Optional: Enable license plate recognition (default: shown below) enabled: False # Optional: The device to run the models on (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information device: CPU # Optional: Set the model size used for text detection. (default: shown below) model_size: small @@ -652,6 +671,8 @@ genai: 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. + model: gemini-1.5-flash # Optional additional args to pass to the GenAI Provider (default: None) provider_options: keep_alive: -1 diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md index fc85ef259..558088646 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -78,17 +78,21 @@ Switching between V1 and V2 requires reindexing your embeddings. The embeddings ### GPU Acceleration -The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU hardware, when available. This depends on the Docker build that is used. +The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU / 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. ```yaml semantic_search: enabled: True model_size: large + # Optional, if using the 'large' model in a multi-GPU installation + device: 0 ``` :::info -If the correct build is used for your GPU and the `large` model is configured, then the GPU will be detected and used automatically. +If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU / NPU will be detected and used automatically. +Specify the `device` option to target a specific GPU in a multi-GPU system (see [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/)). +If you do not specify a device, the first available GPU will be used. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index 8a9454e2c..56024ff01 100644 --- a/docs/docs/frigate/hardware.md +++ b/docs/docs/frigate/hardware.md @@ -58,22 +58,33 @@ Frigate supports multiple different detectors that work on different types of ha - [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices. - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#edge-tpu-detector) +- [MemryX](#memryx-mx3): The MX3 M.2 accelerator module is available in m.2 format allowing for a wide range of compatibility with devices. + - [Supports many model architectures](../../configuration/object_detectors#memryx-mx3) + - Runs best with tiny, small, or medium-size models + **AMD** - [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection - - [Supports limited model architectures](../../configuration/object_detectors#supported-models-1) + - [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models) - Runs best on discrete AMD GPUs +**Apple Silicon** + +- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models) + - Runs well with any size models including large + - Runs via ZMQ proxy which adds some latency, only recommended for local connection + **Intel** - [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. - - [Supports majority of model architectures](../../configuration/object_detectors#supported-models) + - [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models) - Runs best with tiny, small, or medium models **Nvidia** - [TensortRT](#tensorrt---nvidia-gpu): TensorRT can run on Nvidia GPUs and Jetson devices. - - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#supported-models-2) + - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models) - Runs well with any size models including large **Rockchip** @@ -173,17 +184,56 @@ Inference speeds will vary greatly depending on the GPU and the model used. | RTX A4000 | | 320: ~ 15 ms | | | Tesla P40 | | 320: ~ 105 ms | | +### Apple Silicon + +With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon. + +:::warning + +Apple Silicon can not run within a container, so a ZMQ proxy is utilized to communicate with [the Apple Silicon Frigate detector](https://github.com/frigate-nvr/apple-silicon-detector) which runs on the host. This should add minimal latency when run on the same device. + +::: + +| Name | YOLOv9 Inference Time | +| --------- | ---------------------- | +| M3 Pro | t-320: 6 ms s-320: 8ms | +| M1 | s-320: 9ms | + ### ROCm - AMD GPU -With the [rocm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs. +With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs. | Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | | --------- | --------------------- | ------------------------- | | AMD 780M | ~ 14 ms | 320: ~ 25 ms 640: ~ 50 ms | -| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms | ## Community Supported Detectors +### MemryX MX3 + +Frigate supports the MemryX MX3 M.2 AI Acceleration Module on compatible hardware platforms, including both x86 (Intel/AMD) and ARM-based SBCs such as Raspberry Pi 5. + +A single MemryX MX3 module is capable of handling multiple camera streams using the default models, making it sufficient for most users. For larger deployments with more cameras or bigger models, multiple MX3 modules can be used. Frigate supports multi-detector configurations, allowing you to connect multiple MX3 modules to scale inference capacity. + +Detailed information is available [in the detector docs](/configuration/object_detectors#memryx-mx3). + +**Default Model Configuration:** + +- Default model is **YOLO-NAS-Small**. + +The MX3 is a pipelined architecture, where the maximum frames per second supported (and thus supported number of cameras) cannot be calculated as `1/latency` (1/"Inference Time") and is measured separately. When estimating how many camera streams you may support with your configuration, use the **MX3 Total FPS** column to approximate of the detector's limit, not the Inference Time. + +| Model | Input Size | MX3 Inference Time | MX3 Total FPS | +|----------------------|------------|--------------------|---------------| +| YOLO-NAS-Small | 320 | ~ 9 ms | ~ 378 | +| YOLO-NAS-Small | 640 | ~ 21 ms | ~ 138 | +| YOLOv9s | 320 | ~ 16 ms | ~ 382 | +| YOLOv9s | 640 | ~ 41 ms | ~ 110 | +| YOLOX-Small | 640 | ~ 16 ms | ~ 263 | +| SSDlite MobileNet v2 | 320 | ~ 5 ms | ~ 1056 | + +Inference speeds may vary depending on the host platform. The above data was measured on an **Intel 13700 CPU**. Platforms like Raspberry Pi, Orange Pi, and other ARM-based SBCs have different levels of processing capability, which may limit total FPS. + ### Nvidia Jetson Frigate supports all Jetson boards, from the inexpensive Jetson Nano to the powerful Jetson Orin AGX. It will [make use of the Jetson's hardware media engine](/configuration/hardware_acceleration_video#nvidia-jetson-orin-agx-orin-nx-orin-nano-xavier-agx-xavier-nx-tx2-tx1-nano) when configured with the [appropriate presets](/configuration/ffmpeg_presets#hwaccel-presets), and will make use of the Jetson's GPU and DLA for object detection when configured with the [TensorRT detector](/configuration/object_detectors#nvidia-tensorrt-detector). diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index c94daba45..13e66de9e 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -132,6 +132,77 @@ If you are using `docker run`, add this option to your command `--device /dev/ha Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8l) to complete the setup. +### MemryX MX3 + +The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVMe SSD), and supports a variety of configurations: +- x86 (Intel/AMD) PCs +- Raspberry Pi 5 +- Orange Pi 5 Plus/Max +- Multi-M.2 PCIe carrier cards + +#### Configuration + + +#### Installation + +To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/get_started/hardware_setup.html). + +Then follow these steps for installing the correct driver/runtime configuration: + +1. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/dev/docker/memryx/user_installation.sh). +2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh` +3. Run the script with `./user_installation.sh` +4. **Restart your computer** to complete driver installation. + +#### Setup + +To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable` + +Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file: + +```yaml +devices: + - /dev/memx0 +``` + +During configuration, you must run Docker in privileged mode and ensure the container can access the max-manager. + +In your `docker-compose.yml`, also add: + +```yaml +privileged: true + +volumes: + /run/mxa_manager:/run/mxa_manager +``` + +If you can't use Docker Compose, you can run the container with something similar to this: + +```bash + docker run -d \ + --name frigate-memx \ + --restart=unless-stopped \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --shm-size=256m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -v /run/mxa_manager:/run/mxa_manager \ + -e FRIGATE_RTSP_PASSWORD='password' \ + --privileged=true \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 5000:5000 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + --device /dev/memx0 \ + ghcr.io/blakeblackshear/frigate:stable +``` + +#### Configuration + +Finally, configure [hardware object detection](/configuration/object_detectors#memryx-mx3) to complete the setup. + ### Rockchip platform Make sure that you use a linux distribution that comes with the rockchip BSP kernel 5.10 or 6.1 and necessary drivers (especially rkvdec2 and rknpu). To check, enter the following commands: diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index ba1e1302f..2c4b4fc74 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -238,6 +238,14 @@ Topic with current state of notifications. Published values are `ON` and `OFF`. ## Frigate Camera Topics +### `frigate///status` + +Publishes the current health status of each role that is enabled (`audio`, `detect`, `record`). Possible values are: + +- `online`: Stream is running and being processed +- `offline`: Stream is offline and is being restarted +- `disabled`: Camera is currently disabled + ### `frigate//` Publishes the count of objects for the camera for use as a sensor in Home Assistant. diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 9459c4ac8..5bdd66926 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -217,15 +217,23 @@ def require_role(required_roles: List[str]): if not roles: raise HTTPException(status_code=403, detail="Role not provided") - # Check if any role matches required_roles - if not any(role in required_roles for role in roles): + # enforce VALID_ROLES + valid_roles = [r for r in roles if r in VALID_ROLES] + if not valid_roles: raise HTTPException( status_code=403, - detail=f"Role {', '.join(roles)} not authorized. Required: {', '.join(required_roles)}", + detail=f"No valid roles found in {roles}. Required: {', '.join(required_roles)}", ) - # Return the first matching role - return next((role for role in roles if role in required_roles), roles[0]) + if not any(role in required_roles for role in valid_roles): + raise HTTPException( + status_code=403, + detail=f"Role {', '.join(valid_roles)} not authorized. Required: {', '.join(required_roles)}", + ) + + return next( + (role for role in valid_roles if role in required_roles), valid_roles[0] + ) return role_checker @@ -266,22 +274,38 @@ def auth(request: Request): else "anonymous" ) + # start with default_role + role = proxy_config.default_role + + # first try: explicit role header role_header = proxy_config.header_map.role - role = ( - request.headers.get(role_header, default=proxy_config.default_role) - if role_header - else proxy_config.default_role - ) - - # if comma-separated with "admin", use "admin", - # if comma-separated with "viewer", use "viewer", - # else use default role - - roles = [r.strip() for r in role.split(proxy_config.separator)] if role else [] - success_response.headers["remote-role"] = next( - (r for r in VALID_ROLES if r in roles), proxy_config.default_role - ) + if role_header: + raw_value = request.headers.get(role_header, "") + if proxy_config.header_map.role_map and raw_value: + # treat as group claim + groups = [ + g.strip() + for g in raw_value.replace(" ", ",").split(",") + if g.strip() + ] + for ( + candidate_role, + required_groups, + ) in proxy_config.header_map.role_map.items(): + if any(group in groups for group in required_groups): + role = candidate_role + break + elif raw_value: + normalized_role = raw_value.strip().lower() + if normalized_role in VALID_ROLES: + role = normalized_role + else: + logger.warning( + f"Provided proxy role header contains invalid value '{raw_value}'. Using default role '{proxy_config.default_role}'." + ) + role = proxy_config.default_role + success_response.headers["remote-role"] = role return success_response # now apply authentication diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 1265f3af9..d56f6919d 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,8 +1,10 @@ import logging +import re from typing import Optional from fastapi import FastAPI, Request from fastapi.responses import JSONResponse +from joserfc.jwk import OctKey from playhouse.sqliteq import SqliteQueueDatabase from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded @@ -130,6 +132,26 @@ def create_fastapi_app( app.stats_emitter = stats_emitter app.event_metadata_updater = event_metadata_updater app.config_publisher = config_publisher - app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None + + if frigate_config.auth.enabled: + secret = get_jwt_secret() + key_bytes = None + if isinstance(secret, str): + # If the secret looks like hex (e.g., generated by secrets.token_hex), use raw bytes + if len(secret) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", secret or ""): + try: + key_bytes = bytes.fromhex(secret) + except ValueError: + key_bytes = secret.encode("utf-8") + else: + key_bytes = secret.encode("utf-8") + elif isinstance(secret, (bytes, bytearray)): + key_bytes = bytes(secret) + else: + key_bytes = str(secret).encode("utf-8") + + app.jwt_token = OctKey.import_key(key_bytes) + else: + app.jwt_token = None return app diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index e10730931..93e8bcd23 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -1,10 +1,21 @@ """Manage camera activity and updating listeners.""" +import datetime +import json +import logging +import random +import string from collections import Counter from typing import Any, Callable +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) from frigate.config import CameraConfig, FrigateConfig +logger = logging.getLogger(__name__) + class CameraActivityManager: def __init__( @@ -139,3 +150,106 @@ class CameraActivityManager: if any_changed: self.publish(f"{camera}/all", sum(list(all_objects.values()))) self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) + + +class AudioActivityManager: + def __init__( + self, config: FrigateConfig, publish: Callable[[str, Any], None] + ) -> None: + self.config = config + self.publish = publish + self.current_audio_detections: dict[str, dict[str, dict[str, Any]]] = {} + self.event_metadata_publisher = EventMetadataPublisher() + + for camera_config in config.cameras.values(): + if not camera_config.audio.enabled_in_config: + continue + + self.__init_camera(camera_config) + + def __init_camera(self, camera_config: CameraConfig) -> None: + self.current_audio_detections[camera_config.name] = {} + + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + now = datetime.datetime.now().timestamp() + + for camera in new_activity.keys(): + # handle cameras that were added dynamically + if camera not in self.current_audio_detections: + self.__init_camera(self.config.cameras[camera]) + + new_detections = new_activity[camera].get("detections", []) + if self.compare_audio_activity(camera, new_detections, now): + logger.debug(f"Audio detections for {camera}: {new_activity}") + self.publish( + "audio_detections", + json.dumps(self.current_audio_detections), + ) + + def compare_audio_activity( + self, camera: str, new_detections: list[tuple[str, float]], now: float + ) -> None: + max_not_heard = self.config.cameras[camera].audio.max_not_heard + current = self.current_audio_detections[camera] + + any_changed = False + + for label, score in new_detections: + any_changed = True + if label in current: + current[label]["last_detection"] = now + current[label]["score"] = score + else: + rand_id = "".join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + event_id = f"{now}-{rand_id}" + self.publish(f"{camera}/audio/{label}", "ON") + + self.event_metadata_publisher.publish( + ( + now, + camera, + label, + event_id, + True, + score, + None, + None, + "audio", + {}, + ), + EventMetadataTypeEnum.manual_event_create.value, + ) + current[label] = { + "id": event_id, + "score": score, + "last_detection": now, + } + + # expire detections + for label in list(current.keys()): + if now - current[label]["last_detection"] > max_not_heard: + any_changed = True + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] + + return any_changed + + def expire_all(self, camera: str) -> None: + now = datetime.datetime.now().timestamp() + current = self.current_audio_detections.get(camera, {}) + + for label in list(current.keys()): + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index 5bd97136c..865fe4725 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -2,8 +2,6 @@ import logging import multiprocessing as mp -import os -import shutil import threading from multiprocessing import Queue from multiprocessing.managers import DictProxy, SyncManager @@ -16,11 +14,11 @@ from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateSubscriber, ) -from frigate.const import SHM_FRAMES_VAR from frigate.models import Regions from frigate.util.builtin import empty_and_close_queue from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory from frigate.util.object import get_camera_regions_grid +from frigate.util.services import calculate_shm_requirements from frigate.video import CameraCapture, CameraTracker logger = logging.getLogger(__name__) @@ -74,53 +72,25 @@ class CameraMaintainer(threading.Thread): ) def __calculate_shm_frame_count(self) -> int: - total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) + shm_stats = calculate_shm_requirements(self.config) - # required for log files + nginx cache - min_req_shm = 40 + 10 - - if self.config.birdseye.restream: - min_req_shm += 8 - - available_shm = total_shm - min_req_shm - cam_total_frame_size = 0.0 - - for camera in self.config.cameras.values(): - if ( - camera.enabled_in_config - and camera.detect.width - and camera.detect.height - ): - cam_total_frame_size += round( - (camera.detect.width * camera.detect.height * 1.5 + 270480) - / 1048576, - 1, - ) - - # leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them. - cam_total_frame_size += 2 * round( - (1280 * 720 * 1.5 + 270480) / 1048576, - 1, - ) - - if cam_total_frame_size == 0.0: + if not shm_stats: + # /dev/shm not available return 0 - shm_frame_count = min( - int(os.environ.get(SHM_FRAMES_VAR, "50")), - int(available_shm / (cam_total_frame_size)), - ) - logger.debug( - f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {shm_frame_count} frames for each camera in SHM" + f"Calculated total camera size {shm_stats['available']} / " + f"{shm_stats['camera_frame_size']} :: {shm_stats['shm_frame_count']} " + f"frames for each camera in SHM" ) - if shm_frame_count < 20: + if shm_stats["shm_frame_count"] < 20: logger.warning( - f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size * 20)}MB." + f"The current SHM size of {shm_stats['total']}MB is too small, " + f"recommend increasing it to at least {shm_stats['min_shm']}MB." ) - return shm_frame_count + return shm_stats["shm_frame_count"] def __start_camera_processor( self, name: str, config: CameraConfig, runtime: bool = False diff --git a/frigate/camera/state.py b/frigate/camera/state.py index a1041e95b..328c7bd23 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -54,7 +54,7 @@ class CameraState: self.ptz_autotracker_thread = ptz_autotracker_thread self.prev_enabled = self.camera_config.enabled - def get_current_frame(self, draw_options: dict[str, Any] = {}): + def get_current_frame(self, draw_options: dict[str, Any] = {}) -> np.ndarray: with self.current_frame_lock: frame_copy = np.copy(self._current_frame) frame_time = self.current_frame_time @@ -272,7 +272,7 @@ class CameraState: def finished(self, obj_id): del self.tracked_objects[obj_id] - def on(self, event_type: str, callback: Callable[[dict], None]): + def on(self, event_type: str, callback: Callable): self.callbacks[event_type].append(callback) def update( diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 93956068c..235693c8c 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -6,7 +6,7 @@ import logging from typing import Any, Callable, Optional, cast from frigate.camera import PTZMetrics -from frigate.camera.activity_manager import CameraActivityManager +from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager from frigate.comms.base_communicator import Communicator from frigate.comms.webpush import WebPushClient from frigate.config import BirdseyeModeEnum, FrigateConfig @@ -17,10 +17,12 @@ from frigate.config.camera.updater import ( ) from frigate.const import ( CLEAR_ONGOING_REVIEW_SEGMENTS, + EXPIRE_AUDIO_ACTIVITY, INSERT_MANY_RECORDINGS, INSERT_PREVIEW, NOTIFICATION_TEST, REQUEST_REGION_GRID, + UPDATE_AUDIO_ACTIVITY, UPDATE_BIRDSEYE_LAYOUT, UPDATE_CAMERA_ACTIVITY, UPDATE_EMBEDDINGS_REINDEX_PROGRESS, @@ -55,6 +57,7 @@ class Dispatcher: self.ptz_metrics = ptz_metrics self.comms = communicators self.camera_activity = CameraActivityManager(config, self.publish) + self.audio_activity = AudioActivityManager(config, self.publish) self.model_state: dict[str, ModelStatusTypesEnum] = {} self.embeddings_reindex: dict[str, Any] = {} self.birdseye_layout: dict[str, Any] = {} @@ -135,6 +138,12 @@ class Dispatcher: def handle_update_camera_activity() -> None: self.camera_activity.update_activity(payload) + def handle_update_audio_activity() -> None: + self.audio_activity.update_activity(payload) + + def handle_expire_audio_activity() -> None: + self.audio_activity.expire_all(payload) + def handle_update_event_description() -> None: event: Event = Event.get(Event.id == payload["id"]) cast(dict, event.data)["description"] = payload["description"] @@ -192,6 +201,7 @@ class Dispatcher: def handle_on_connect() -> None: camera_status = self.camera_activity.last_camera_activity.copy() + audio_detections = self.audio_activity.current_audio_detections.copy() cameras_with_status = camera_status.keys() for camera in self.config.cameras.keys(): @@ -234,6 +244,7 @@ class Dispatcher: json.dumps(self.embeddings_reindex.copy()), ) self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) + self.publish("audio_detections", json.dumps(audio_detections)) def handle_notification_test() -> None: self.publish("notification_test", "Test notification") @@ -246,6 +257,8 @@ class Dispatcher: UPSERT_REVIEW_SEGMENT: handle_upsert_review_segment, CLEAR_ONGOING_REVIEW_SEGMENTS: handle_clear_ongoing_review_segments, UPDATE_CAMERA_ACTIVITY: handle_update_camera_activity, + UPDATE_AUDIO_ACTIVITY: handle_update_audio_activity, + EXPIRE_AUDIO_ACTIVITY: handle_expire_audio_activity, UPDATE_EVENT_DESCRIPTION: handle_update_event_description, UPDATE_REVIEW_DESCRIPTION: handle_update_review_description, UPDATE_MODEL_STATE: handle_update_model_state, diff --git a/frigate/comms/events_updater.py b/frigate/comms/events_updater.py index f25f760ac..cfd958d2c 100644 --- a/frigate/comms/events_updater.py +++ b/frigate/comms/events_updater.py @@ -8,7 +8,7 @@ from .zmq_proxy import Publisher, Subscriber class EventUpdatePublisher( - Publisher[tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]]] + Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]] ): """Publishes events (objects, audio, manual).""" @@ -19,7 +19,7 @@ class EventUpdatePublisher( def publish( self, - payload: tuple[EventTypeEnum, EventStateEnum, str, str, dict[str, Any]], + payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]], sub_topic: str = "", ) -> None: super().publish(payload, sub_topic) diff --git a/frigate/comms/zmq_proxy.py b/frigate/comms/zmq_proxy.py index 72a61d814..29329ec59 100644 --- a/frigate/comms/zmq_proxy.py +++ b/frigate/comms/zmq_proxy.py @@ -2,7 +2,7 @@ import json import threading -from typing import Any, Generic, Optional, TypeVar +from typing import Generic, TypeVar import zmq @@ -70,7 +70,7 @@ class Publisher(Generic[T]): self.context.destroy() -class Subscriber: +class Subscriber(Generic[T]): """Receives messages.""" topic_base: str = "" @@ -82,9 +82,7 @@ class Subscriber: self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) self.socket.connect(SOCKET_SUB) - def check_for_update( - self, timeout: float | None = FAST_QUEUE_TIMEOUT - ) -> tuple[str, Any] | tuple[None, None] | None: + def check_for_update(self, timeout: float | None = FAST_QUEUE_TIMEOUT) -> T | None: """Returns message or None if no update.""" try: has_update, _, _ = zmq.select([self.socket], [], [], timeout) @@ -101,7 +99,5 @@ class Subscriber: self.socket.close() self.context.destroy() - def _return_object( - self, topic: str, payload: Optional[tuple[str, Any]] - ) -> tuple[str, Any] | tuple[None, None] | None: + def _return_object(self, topic: str, payload: T | None) -> T | None: return payload diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 234113dd2..63e89421c 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -130,6 +130,11 @@ class SemanticSearchConfig(FrigateBaseModel): model_size: str = Field( default="small", title="The size of the embeddings model used." ) + device: Optional[str] = Field( + default=None, + title="The device key to use for semantic search.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) class TriggerConfig(FrigateBaseModel): @@ -196,6 +201,11 @@ class FaceRecognitionConfig(FrigateBaseModel): blur_confidence_filter: bool = Field( default=True, title="Apply blur quality filter to face confidence." ) + device: Optional[str] = Field( + default=None, + title="The device key to use for face recognition.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) class CameraFaceRecognitionConfig(FrigateBaseModel): @@ -209,10 +219,6 @@ class CameraFaceRecognitionConfig(FrigateBaseModel): class LicensePlateRecognitionConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable license plate recognition.") - device: Optional[EnrichmentsDeviceEnum] = Field( - default=EnrichmentsDeviceEnum.CPU, - title="The device used for license plate recognition.", - ) model_size: str = Field( default="small", title="The size of the embeddings model used." ) @@ -258,6 +264,11 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): default=False, title="Save plates captured for LPR for debugging purposes.", ) + device: Optional[str] = Field( + default=None, + title="The device key to use for LPR.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) class CameraLicensePlateRecognitionConfig(FrigateBaseModel): diff --git a/frigate/config/config.py b/frigate/config/config.py index de41c1d24..dd84639d3 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -64,6 +64,7 @@ from .database import DatabaseConfig from .env import EnvVars from .logger import LoggerConfig from .mqtt import MqttConfig +from .network import NetworkingConfig from .proxy import ProxyConfig from .telemetry import TelemetryConfig from .tls import TlsConfig @@ -334,6 +335,9 @@ class FrigateConfig(FrigateBaseModel): notifications: NotificationConfig = Field( default_factory=NotificationConfig, title="Global notification configuration." ) + networking: NetworkingConfig = Field( + default_factory=NetworkingConfig, title="Networking configuration" + ) proxy: ProxyConfig = Field( default_factory=ProxyConfig, title="Proxy configuration." ) diff --git a/frigate/config/network.py b/frigate/config/network.py new file mode 100644 index 000000000..c8b3cfd1c --- /dev/null +++ b/frigate/config/network.py @@ -0,0 +1,13 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["IPv6Config", "NetworkingConfig"] + + +class IPv6Config(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable IPv6 for port 5000 and/or 8971") + + +class NetworkingConfig(FrigateBaseModel): + ipv6: IPv6Config = Field(default_factory=IPv6Config, title="Network configuration") diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py index 68bd400e7..a46b7b897 100644 --- a/frigate/config/proxy.py +++ b/frigate/config/proxy.py @@ -16,6 +16,10 @@ class HeaderMappingConfig(FrigateBaseModel): default=None, title="Header name from upstream proxy to identify user role.", ) + role_map: Optional[dict[str, list[str]]] = Field( + default_factory=dict, + title=("Mapping of Frigate roles to upstream group values. "), + ) class ProxyConfig(FrigateBaseModel): diff --git a/frigate/const.py b/frigate/const.py index 67f2fd907..5710966bf 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -74,6 +74,7 @@ FFMPEG_HWACCEL_NVIDIA = "preset-nvidia" FFMPEG_HWACCEL_VAAPI = "preset-vaapi" FFMPEG_HWACCEL_VULKAN = "preset-vulkan" FFMPEG_HWACCEL_RKMPP = "preset-rkmpp" +FFMPEG_HWACCEL_AMF = "preset-amd-amf" FFMPEG_HVC1_ARGS = ["-tag:v", "hvc1"] # Regex constants @@ -110,6 +111,8 @@ REQUEST_REGION_GRID = "request_region_grid" UPSERT_REVIEW_SEGMENT = "upsert_review_segment" CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" UPDATE_CAMERA_ACTIVITY = "update_camera_activity" +UPDATE_AUDIO_ACTIVITY = "update_audio_activity" +EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity" UPDATE_EVENT_DESCRIPTION = "update_event_description" UPDATE_REVIEW_DESCRIPTION = "update_review_description" UPDATE_MODEL_STATE = "update_model_state" diff --git a/frigate/data_processing/common/face/model.py b/frigate/data_processing/common/face/model.py index f230a1b2c..21de37768 100644 --- a/frigate/data_processing/common/face/model.py +++ b/frigate/data_processing/common/face/model.py @@ -269,7 +269,7 @@ class ArcFaceRecognizer(FaceRecognizer): def __init__(self, config: FrigateConfig): super().__init__(config) self.mean_embs: dict[int, np.ndarray] = {} - self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding() + self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding(config.face_recognition) self.model_builder_queue: queue.Queue | None = None def clear(self) -> None: diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index a9e94ac92..c0cd50894 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -171,7 +171,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): # don't run for non person objects if obj_data.get("label") != "person": - logger.debug("Not a processing face for non person object.") + logger.debug("Not processing face for a non person object.") return # don't overwrite sub label for objects that have a sub label diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py new file mode 100644 index 000000000..2c741e0f6 --- /dev/null +++ b/frigate/detectors/plugins/memryx.py @@ -0,0 +1,731 @@ +import glob +import logging +import os +import shutil +import time +import urllib.request +import zipfile +from queue import Queue + +import cv2 +import numpy as np +from pydantic import BaseModel, Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + ModelTypeEnum, +) +from frigate.util.model import post_process_yolo + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "memryx" + + +# Configuration class for model settings +class ModelConfig(BaseModel): + path: str = Field(default=None, title="Model Path") # Path to the DFP file + labelmap_path: str = Field(default=None, title="Path to Label Map") + + +class MemryXDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default="PCIe", title="Device Path") + + +class MemryXDetector(DetectionApi): + type_key = DETECTOR_KEY # Set the type key + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yolonas, + ModelTypeEnum.yologeneric, # Treated as yolov9 in MemryX implementation + ModelTypeEnum.yolox, + ] + + def __init__(self, detector_config): + """Initialize MemryX detector with the provided configuration.""" + try: + # Import MemryX SDK + from memryx import AsyncAccl + except ModuleNotFoundError: + raise ImportError( + "MemryX SDK is not installed. Install it and set up MIX environment." + ) + return + + model_cfg = getattr(detector_config, "model", None) + + # Check if model_type was explicitly set by the user + if "model_type" in getattr(model_cfg, "__fields_set__", set()): + detector_config.model.model_type = model_cfg.model_type + else: + logger.info( + "model_type not set in config — defaulting to yolonas for MemryX." + ) + detector_config.model.model_type = ModelTypeEnum.yolonas + + self.capture_queue = Queue(maxsize=10) + self.output_queue = Queue(maxsize=10) + self.capture_id_queue = Queue(maxsize=10) + self.logger = logger + + self.memx_model_path = detector_config.model.path # Path to .dfp file + self.memx_post_model = None # Path to .post file + self.expected_post_model = None + + self.memx_device_path = detector_config.device # Device path + # Parse the device string to split PCIe: + device_str = self.memx_device_path + self.device_id = [] + self.device_id.append(int(device_str.split(":")[1])) + + self.memx_model_height = detector_config.model.height + self.memx_model_width = detector_config.model.width + self.memx_model_type = detector_config.model.model_type + + self.cache_dir = "/memryx_models" + + if self.memx_model_type == ModelTypeEnum.yologeneric: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_640.zip", + "yolov9_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + ) + self.expected_dfp_model = "YOLO_v9_small_onnx.dfp" + + elif self.memx_model_type == ModelTypeEnum.yolonas: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_640.zip", + "yolonas_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + ) + self.expected_dfp_model = "yolo_nas_s.dfp" + self.expected_post_model = "yolo_nas_s_post.onnx" + + elif self.memx_model_type == ModelTypeEnum.yolox: + self.model_folder = "yolox" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/yolox.zip" + ) + self.expected_dfp_model = "YOLOX_640_640_3_onnx.dfp" + self.set_strides_grids() + + elif self.memx_model_type == ModelTypeEnum.ssd: + self.model_folder = "ssd" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/ssd.zip" + ) + self.expected_dfp_model = "SSDlite_MobileNet_v2_320_320_3_onnx.dfp" + self.expected_post_model = "SSDlite_MobileNet_v2_320_320_3_onnx_post.onnx" + + self.check_and_prepare_model() + logger.info( + f"Initializing MemryX with model: {self.memx_model_path} on device {self.memx_device_path}" + ) + + try: + # Load MemryX Model + logger.info(f"dfp path: {self.memx_model_path}") + + # Initialization code + # Load MemryX Model with a device target + self.accl = AsyncAccl( + self.memx_model_path, + device_ids=self.device_id, # AsyncAccl device ids + local_mode=True, + ) + + # Models that use cropped post-processing sections (YOLO-NAS and SSD) + # --> These will be moved to pure numpy in the future to improve performance on low-end CPUs + if self.memx_post_model: + self.accl.set_postprocessing_model(self.memx_post_model, model_idx=0) + + self.accl.connect_input(self.process_input) + self.accl.connect_output(self.process_output) + + logger.info( + f"Loaded MemryX model from {self.memx_model_path} and {self.memx_post_model}" + ) + + except Exception as e: + logger.error(f"Failed to initialize MemryX model: {e}") + raise + + def 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) + + # ---------- CASE 1: user provided a custom model path ---------- + if self.memx_model_path: + if not self.memx_model_path.endswith(".zip"): + raise ValueError( + f"Invalid model path: {self.memx_model_path}. " + "Only .zip files are supported. Please provide a .zip model archive." + ) + if not os.path.exists(self.memx_model_path): + raise FileNotFoundError( + f"Custom model zip not found: {self.memx_model_path}" + ) + + logger.info(f"User provided zip model: {self.memx_model_path}") + + # Extract custom zip into a separate area so it never clashes with MemryX cache + custom_dir = os.path.join( + self.cache_dir, "custom_models", self.model_folder + ) + if os.path.isdir(custom_dir): + shutil.rmtree(custom_dir) + os.makedirs(custom_dir, exist_ok=True) + + with zipfile.ZipFile(self.memx_model_path, "r") as zip_ref: + zip_ref.extractall(custom_dir) + logger.info(f"Custom model extracted to {custom_dir}.") + + # Find .dfp and optional *_post.onnx recursively + dfp_candidates = glob.glob( + os.path.join(custom_dir, "**", "*.dfp"), recursive=True + ) + post_candidates = glob.glob( + os.path.join(custom_dir, "**", "*_post.onnx"), recursive=True + ) + + if not dfp_candidates: + raise FileNotFoundError( + "No .dfp file found in custom model zip after extraction." + ) + + self.memx_model_path = dfp_candidates[0] + + # Handle post model requirements by model type + if self.memx_model_type in [ + ModelTypeEnum.yologeneric, + ModelTypeEnum.yolonas, + ModelTypeEnum.ssd, + ]: + if not post_candidates: + raise FileNotFoundError( + f"No *_post.onnx file found in custom model zip for {self.memx_model_type.name}." + ) + self.memx_post_model = post_candidates[0] + elif self.memx_model_type == ModelTypeEnum.yolox: + # Explicitly ignore any post model even if present + self.memx_post_model = None + else: + # Future model types can optionally use post if present + self.memx_post_model = post_candidates[0] if post_candidates else None + + logger.info(f"Using custom model: {self.memx_model_path}") + return + + # ---------- CASE 2: no custom model path -> use MemryX cached models ---------- + model_subdir = os.path.join(self.cache_dir, self.model_folder) + dfp_path = os.path.join(model_subdir, self.expected_dfp_model) + post_path = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + dfp_exists = os.path.exists(dfp_path) + post_exists = os.path.exists(post_path) if post_path else True + + if dfp_exists and post_exists: + logger.info("Using cached models.") + self.memx_model_path = dfp_path + self.memx_post_model = post_path + if self.memx_model_type == ModelTypeEnum.yologeneric: + self.load_yolo_constants() + return + + # ---------- CASE 3: download MemryX model (no cache) ---------- + logger.info( + f"Model files not found locally. Downloading from {self.model_url}..." + ) + zip_path = os.path.join(self.cache_dir, f"{self.model_folder}.zip") + + try: + if not os.path.exists(zip_path): + urllib.request.urlretrieve(self.model_url, zip_path) + logger.info(f"Model ZIP downloaded to {zip_path}. Extracting...") + + if not os.path.exists(model_subdir): + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(self.cache_dir) + logger.info(f"Model extracted to {self.cache_dir}.") + + # Re-assign model paths after extraction + self.memx_model_path = os.path.join(model_subdir, self.expected_dfp_model) + self.memx_post_model = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + if self.memx_model_type == ModelTypeEnum.yologeneric: + self.load_yolo_constants() + + finally: + if os.path.exists(zip_path): + try: + os.remove(zip_path) + logger.info("Cleaned up ZIP file after extraction.") + except Exception as e: + logger.warning(f"Failed to remove downloaded zip {zip_path}: {e}") + + def send_input(self, connection_id, tensor_input: np.ndarray): + """Pre-process (if needed) and send frame to MemryX input queue""" + if tensor_input is None: + raise ValueError("[send_input] No image data provided for inference") + + if self.memx_model_type == ModelTypeEnum.yolonas: + if tensor_input.ndim == 4 and tensor_input.shape[1:] == (320, 320, 3): + logger.debug("Transposing tensor from NHWC to NCHW for YOLO-NAS") + tensor_input = np.transpose( + tensor_input, (0, 3, 1, 2) + ) # (1, H, W, C) → (1, C, H, W) + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + + if self.memx_model_type == ModelTypeEnum.yolox: + # Remove batch dim → (3, 640, 640) + tensor_input = tensor_input.squeeze(0) + + # Convert CHW to HWC for OpenCV + tensor_input = np.transpose(tensor_input, (1, 2, 0)) # (640, 640, 3) + + padded_img = np.ones((640, 640, 3), dtype=np.uint8) * 114 + + scale = min( + 640 / float(tensor_input.shape[0]), 640 / float(tensor_input.shape[1]) + ) + sx, sy = ( + int(tensor_input.shape[1] * scale), + int(tensor_input.shape[0] * scale), + ) + + resized_img = cv2.resize( + tensor_input, (sx, sy), interpolation=cv2.INTER_LINEAR + ) + padded_img[:sy, :sx] = resized_img.astype(np.uint8) + + # Step 4: Slice the padded image into 4 quadrants and concatenate them into 12 channels + x0 = padded_img[0::2, 0::2, :] # Top-left + x1 = padded_img[1::2, 0::2, :] # Bottom-left + x2 = padded_img[0::2, 1::2, :] # Top-right + x3 = padded_img[1::2, 1::2, :] # Bottom-right + + # Step 5: Concatenate along the channel dimension (axis 2) + concatenated_img = np.concatenate([x0, x1, x2, x3], axis=2) + tensor_input = concatenated_img.astype(np.float32) + # Convert to CHW format (12, 320, 320) + tensor_input = np.transpose(tensor_input, (2, 0, 1)) + + # Add batch dimension → (1, 12, 320, 320) + tensor_input = np.expand_dims(tensor_input, axis=0) + + # Send frame to MemryX for processing + self.capture_queue.put(tensor_input) + self.capture_id_queue.put(connection_id) + + def process_input(self): + """Input callback function: wait for frames in the input queue, preprocess, and send to MX3 (return)""" + while True: + try: + # Wait for a frame from the queue (blocking call) + frame = self.capture_queue.get( + block=True + ) # Blocks until data is available + + return frame + + except Exception as e: + logger.info(f"[process_input] Error processing input: {e}") + time.sleep(0.1) # Prevent busy waiting in case of error + + def receive_output(self): + """Retrieve processed results from MemryX output queue + a copy of the original frame""" + connection_id = ( + self.capture_id_queue.get() + ) # Get the corresponding connection ID + detections = self.output_queue.get() # Get detections from MemryX + + return connection_id, detections + + def post_process_yolonas(self, output): + predictions = output[0] + + detections = np.zeros((20, 6), np.float32) + + for i, prediction in enumerate(predictions): + if i == 20: + break + + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + + if class_id < 0: + break + + detections[i] = [ + class_id, + confidence, + y_min / self.memx_model_height, + x_min / self.memx_model_width, + y_max / self.memx_model_height, + x_max / self.memx_model_width, + ] + + # Return the list of final detections + self.output_queue.put(detections) + + def process_yolo(self, class_id, conf, pos): + """ + Takes in class ID, confidence score, and array of [x, y, w, h] that describes detection position, + returns an array that's easily passable back to Frigate. + """ + return [ + class_id, # class ID + conf, # confidence score + (pos[1] - (pos[3] / 2)) / self.memx_model_height, # y_min + (pos[0] - (pos[2] / 2)) / self.memx_model_width, # x_min + (pos[1] + (pos[3] / 2)) / self.memx_model_height, # y_max + (pos[0] + (pos[2] / 2)) / self.memx_model_width, # x_max + ] + + def set_strides_grids(self): + grids = [] + expanded_strides = [] + + strides = [8, 16, 32] + + hsize_list = [self.memx_model_height // stride for stride in strides] + wsize_list = [self.memx_model_width // stride for stride in strides] + + for hsize, wsize, stride in zip(hsize_list, wsize_list, strides): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + self.grids = np.concatenate(grids, 1) + self.expanded_strides = np.concatenate(expanded_strides, 1) + + def sigmoid(self, x: np.ndarray) -> np.ndarray: + return 1 / (1 + np.exp(-x)) + + def onnx_concat(self, inputs: list, axis: int) -> np.ndarray: + # Ensure all inputs are numpy arrays + if not all(isinstance(x, np.ndarray) for x in inputs): + raise TypeError("All inputs must be numpy arrays.") + + # Ensure shapes match on non-concat axes + ref_shape = list(inputs[0].shape) + for i, tensor in enumerate(inputs[1:], start=1): + for ax in range(len(ref_shape)): + if ax == axis: + continue + if tensor.shape[ax] != ref_shape[ax]: + raise ValueError( + f"Shape mismatch at axis {ax} between input[0] and input[{i}]" + ) + + return np.concatenate(inputs, axis=axis) + + def onnx_reshape(self, data: np.ndarray, shape: np.ndarray) -> np.ndarray: + # Ensure shape is a 1D array of integers + target_shape = shape.astype(int).tolist() + + # Use NumPy reshape with dynamic handling of -1 + reshaped = np.reshape(data, target_shape) + + return reshaped + + def post_process_yolox(self, output): + output_785 = output[0] # 785 + output_794 = output[1] # 794 + output_795 = output[2] # 795 + output_811 = output[3] # 811 + output_820 = output[4] # 820 + output_821 = output[5] # 821 + output_837 = output[6] # 837 + output_846 = output[7] # 846 + output_847 = output[8] # 847 + + output_795 = self.sigmoid(output_795) + output_785 = self.sigmoid(output_785) + output_821 = self.sigmoid(output_821) + output_811 = self.sigmoid(output_811) + output_847 = self.sigmoid(output_847) + output_837 = self.sigmoid(output_837) + + concat_1 = self.onnx_concat([output_794, output_795, output_785], axis=1) + concat_2 = self.onnx_concat([output_820, output_821, output_811], axis=1) + concat_3 = self.onnx_concat([output_846, output_847, output_837], axis=1) + + shape = np.array([1, 85, -1], dtype=np.int64) + + reshape_1 = self.onnx_reshape(concat_1, shape) + reshape_2 = self.onnx_reshape(concat_2, shape) + reshape_3 = self.onnx_reshape(concat_3, shape) + + concat_out = self.onnx_concat([reshape_1, reshape_2, reshape_3], axis=2) + + output = concat_out.transpose(0, 2, 1) # 1, 840, 85 + + self.num_classes = output.shape[2] - 5 + + # [x, y, h, w, box_score, class_no_1, ..., class_no_80], + results = output + + results[..., :2] = (results[..., :2] + self.grids) * self.expanded_strides + results[..., 2:4] = np.exp(results[..., 2:4]) * self.expanded_strides + image_pred = results[0, ...] + + class_conf = np.max( + image_pred[:, 5 : 5 + self.num_classes], axis=1, keepdims=True + ) + class_pred = np.argmax(image_pred[:, 5 : 5 + self.num_classes], axis=1) + class_pred = np.expand_dims(class_pred, axis=1) + + conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze() + # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred) + detections = np.concatenate((image_pred[:, :5], class_conf, class_pred), axis=1) + detections = detections[conf_mask] + + # Sort by class confidence (index 5) and keep top 20 detections + ordered = detections[detections[:, 5].argsort()[::-1]][:20] + + # Prepare a final detections array of shape (20, 6) + final_detections = np.zeros((20, 6), np.float32) + for i, object_detected in enumerate(ordered): + final_detections[i] = self.process_yolo( + object_detected[6], object_detected[5], object_detected[:4] + ) + + self.output_queue.put(final_detections) + + def post_process_ssdlite(self, outputs): + dets = outputs[0].squeeze(0) # Shape: (1, num_dets, 5) + labels = outputs[1].squeeze(0) + + detections = [] + + for i in range(dets.shape[0]): + x_min, y_min, x_max, y_max, confidence = dets[i] + class_id = int(labels[i]) # Convert label to integer + + if confidence < 0.45: + continue # Skip detections below threshold + + # Convert coordinates to integers + x_min, y_min, x_max, y_max = map(int, [x_min, y_min, x_max, y_max]) + + # Append valid detections [class_id, confidence, x, y, width, height] + detections.append([class_id, confidence, x_min, y_min, x_max, y_max]) + + final_detections = np.zeros((20, 6), np.float32) + + if len(detections) == 0: + # logger.info("No detections found.") + self.output_queue.put(final_detections) + return + + # Convert to NumPy array + detections = np.array(detections, dtype=np.float32) + + # Apply Non-Maximum Suppression (NMS) + bboxes = detections[:, 2:6].tolist() # (x_min, y_min, width, height) + scores = detections[:, 1].tolist() # Confidence scores + + indices = cv2.dnn.NMSBoxes(bboxes, scores, 0.45, 0.5) + + if len(indices) > 0: + indices = indices.flatten()[:20] # Keep only the top 20 detections + selected_detections = detections[indices] + + # Normalize coordinates AFTER NMS + for i, det in enumerate(selected_detections): + class_id, confidence, x_min, y_min, x_max, y_max = det + + # Normalize coordinates + x_min /= self.memx_model_width + y_min /= self.memx_model_height + x_max /= self.memx_model_width + y_max /= self.memx_model_height + + final_detections[i] = [class_id, confidence, y_min, x_min, y_max, x_max] + + self.output_queue.put(final_detections) + + def onnx_reshape_with_allowzero( + self, data: np.ndarray, shape: np.ndarray, allowzero: int = 0 + ) -> np.ndarray: + shape = shape.astype(int) + input_shape = data.shape + output_shape = [] + + 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) + + # Now let NumPy infer any -1 if needed + reshaped = np.reshape(data, output_shape) + + return reshaped + + 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] + + 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: + return self.post_process_yolonas(outputs) + + elif self.memx_model_type == ModelTypeEnum.yolox: + return self.post_process_yolox(outputs) + + elif self.memx_model_type == ModelTypeEnum.ssd: + return self.post_process_ssdlite(outputs) + + else: + raise Exception( + f"{self.memx_model_type} is currently not supported for memryx. See the docs for more info on supported models." + ) + + def detect_raw(self, tensor_input: np.ndarray): + """Removed synchronous detect_raw() function so that we only use async""" + return 0 diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py index 828507c54..1334018c8 100644 --- a/frigate/detectors/plugins/rknn.py +++ b/frigate/detectors/plugins/rknn.py @@ -12,6 +12,7 @@ from frigate.const import MODEL_CACHE_DIR from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum from frigate.util.model import post_process_yolo +from frigate.util.rknn_converter import auto_convert_model logger = logging.getLogger(__name__) @@ -94,7 +95,31 @@ class Rknn(DetectionApi): # user provided models should be a path and contain a "/" if "/" in model_path: model_props["preset"] = False - model_props["path"] = model_path + + # Check if this is an ONNX model or model without extension that needs conversion + if model_path.endswith(".onnx") or not os.path.splitext(model_path)[1]: + # Try to auto-convert to RKNN format + logger.info( + f"Attempting to auto-convert {model_path} to RKNN format..." + ) + + # Determine model type from config + model_type = self.detector_config.model.model_type + + # Auto-convert the model + converted_path = auto_convert_model(model_path, model_type.value) + + if converted_path: + model_props["path"] = converted_path + logger.info(f"Successfully converted model to: {converted_path}") + else: + # Fall back to original path if conversion fails + logger.warning( + f"Failed to convert {model_path} to RKNN format, using original path" + ) + model_props["path"] = model_path + else: + model_props["path"] = model_path else: model_props["preset"] = True diff --git a/frigate/detectors/plugins/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py new file mode 100644 index 000000000..112176c1a --- /dev/null +++ b/frigate/detectors/plugins/zmq_ipc.py @@ -0,0 +1,151 @@ +import json +import logging +from typing import Any, List + +import numpy as np +import zmq +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "zmq" + + +class ZmqDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + endpoint: str = Field( + default="ipc:///tmp/cache/zmq_detector", title="ZMQ IPC endpoint" + ) + request_timeout_ms: int = Field( + default=200, title="ZMQ request timeout in milliseconds" + ) + linger_ms: int = Field(default=0, title="ZMQ socket linger in milliseconds") + + +class ZmqIpcDetector(DetectionApi): + """ + ZMQ-based detector plugin using a REQ/REP socket over an IPC endpoint. + + Protocol: + - Request is sent as a multipart message: + [ header_json_bytes, tensor_bytes ] + where header is a JSON object containing: + { + "shape": List[int], + "dtype": str, # numpy dtype string, e.g. "uint8", "float32" + } + tensor_bytes are the raw bytes of the numpy array in C-order. + + - Response is expected to be either: + a) Multipart [ header_json_bytes, tensor_bytes ] with header specifying + shape [20,6] and dtype "float32"; or + b) Single frame tensor_bytes of length 20*6*4 bytes (float32). + + On any error or timeout, this detector returns a zero array of shape (20, 6). + """ + + type_key = DETECTOR_KEY + + def __init__(self, detector_config: ZmqDetectorConfig): + super().__init__(detector_config) + + self._context = zmq.Context() + self._endpoint = detector_config.endpoint + self._request_timeout_ms = detector_config.request_timeout_ms + self._linger_ms = detector_config.linger_ms + self._socket = None + self._create_socket() + + # Preallocate zero result for error paths + self._zero_result = np.zeros((20, 6), np.float32) + + def _create_socket(self) -> None: + if self._socket is not None: + try: + self._socket.close(linger=self._linger_ms) + except Exception: + pass + self._socket = self._context.socket(zmq.REQ) + # Apply timeouts and linger so calls don't block indefinitely + self._socket.setsockopt(zmq.RCVTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.SNDTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.LINGER, self._linger_ms) + + logger.debug(f"ZMQ detector connecting to {self._endpoint}") + self._socket.connect(self._endpoint) + + def _build_header(self, tensor_input: np.ndarray) -> bytes: + header: dict[str, Any] = { + "shape": list(tensor_input.shape), + "dtype": str(tensor_input.dtype.name), + } + return json.dumps(header).encode("utf-8") + + def _decode_response(self, frames: List[bytes]) -> np.ndarray: + try: + if len(frames) == 1: + # Single-frame raw float32 (20x6) + buf = frames[0] + if len(buf) != 20 * 6 * 4: + logger.warning( + f"ZMQ detector received unexpected payload size: {len(buf)}" + ) + return self._zero_result + return np.frombuffer(buf, dtype=np.float32).reshape((20, 6)) + + if len(frames) >= 2: + header = json.loads(frames[0].decode("utf-8")) + shape = tuple(header.get("shape", [])) + dtype = np.dtype(header.get("dtype", "float32")) + return np.frombuffer(frames[1], dtype=dtype).reshape(shape) + + logger.warning("ZMQ detector received empty reply") + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector failed to decode response: {exc}") + return self._zero_result + + def detect_raw(self, tensor_input: np.ndarray) -> np.ndarray: + try: + header_bytes = self._build_header(tensor_input) + payload_bytes = memoryview(tensor_input.tobytes(order="C")) + + # Send request + self._socket.send_multipart([header_bytes, payload_bytes]) + + # Receive reply + reply_frames = self._socket.recv_multipart() + detections = self._decode_response(reply_frames) + + # Ensure output shape and dtype are exactly as expected + + return detections + except zmq.Again: + # Timeout + logger.debug("ZMQ detector request timed out; resetting socket") + try: + self._create_socket() + except Exception: + pass + return self._zero_result + except zmq.ZMQError as exc: + logger.error(f"ZMQ detector ZMQError: {exc}; resetting socket") + try: + self._create_socket() + except Exception: + pass + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector unexpected error: {exc}") + return self._zero_result + + def __del__(self) -> None: # pragma: no cover - best-effort cleanup + try: + if self._socket is not None: + self._socket.close(linger=self.detector_config.linger_ms) + except Exception: + pass diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py index a0981f669..788e3e6db 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -112,9 +112,8 @@ class Embeddings: self.embedding = JinaV2Embedding( model_size=self.config.semantic_search.model_size, requestor=self.requestor, - device="GPU" - if self.config.semantic_search.model_size == "large" - else "CPU", + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), ) self.text_embedding = lambda input_data: self.embedding( input_data, embedding_type="text" @@ -131,7 +130,8 @@ class Embeddings: self.vision_embedding = JinaV1ImageEmbedding( model_size=config.semantic_search.model_size, requestor=self.requestor, - device="GPU" if config.semantic_search.model_size == "large" else "CPU", + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), ) def update_stats(self) -> None: diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py index acb4507a2..10d5627d9 100644 --- a/frigate/embeddings/onnx/face_embedding.py +++ b/frigate/embeddings/onnx/face_embedding.py @@ -9,6 +9,7 @@ from frigate.const import MODEL_CACHE_DIR from frigate.log import redirect_output_to_logger from frigate.util.downloader import ModelDownloader +from ...config import FaceRecognitionConfig from .base_embedding import BaseEmbedding from .runner import ONNXModelRunner @@ -111,7 +112,7 @@ class FaceNetEmbedding(BaseEmbedding): class ArcfaceEmbedding(BaseEmbedding): - def __init__(self): + def __init__(self, config: FaceRecognitionConfig): super().__init__( model_name="facedet", model_file="arcface.onnx", @@ -119,6 +120,7 @@ class ArcfaceEmbedding(BaseEmbedding): "arcface.onnx": "https://github.com/NickM-27/facenet-onnx/releases/download/v1.0/arcface.onnx", }, ) + self.config = config self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) self.tokenizer = None self.feature_extractor = None @@ -148,7 +150,7 @@ class ArcfaceEmbedding(BaseEmbedding): self.runner = ONNXModelRunner( os.path.join(self.download_path, self.model_file), - "GPU", + device=self.config.device or "GPU", ) def _preprocess_inputs(self, raw_inputs): diff --git a/frigate/embeddings/onnx/jina_v1_embedding.py b/frigate/embeddings/onnx/jina_v1_embedding.py index b448ec816..d327fa8ba 100644 --- a/frigate/embeddings/onnx/jina_v1_embedding.py +++ b/frigate/embeddings/onnx/jina_v1_embedding.py @@ -128,7 +128,6 @@ class JinaV1TextEmbedding(BaseEmbedding): self.runner = ONNXModelRunner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, ) def _preprocess_inputs(self, raw_inputs): @@ -207,7 +206,6 @@ class JinaV1ImageEmbedding(BaseEmbedding): self.runner = ONNXModelRunner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, ) def _preprocess_inputs(self, raw_inputs): diff --git a/frigate/embeddings/onnx/jina_v2_embedding.py b/frigate/embeddings/onnx/jina_v2_embedding.py index e9def9a07..50b503d76 100644 --- a/frigate/embeddings/onnx/jina_v2_embedding.py +++ b/frigate/embeddings/onnx/jina_v2_embedding.py @@ -128,7 +128,6 @@ class JinaV2Embedding(BaseEmbedding): self.runner = ONNXModelRunner( os.path.join(self.download_path, self.model_file), self.device, - self.model_size, ) def _preprocess_image(self, image_data: bytes | Image.Image) -> np.ndarray: diff --git a/frigate/embeddings/onnx/runner.py b/frigate/embeddings/onnx/runner.py index c34c97a8d..9158be9f4 100644 --- a/frigate/embeddings/onnx/runner.py +++ b/frigate/embeddings/onnx/runner.py @@ -4,10 +4,12 @@ import logging import os.path from typing import Any +import numpy as np import onnxruntime as ort from frigate.const import MODEL_CACHE_DIR from frigate.util.model import get_ort_providers +from frigate.util.rknn_converter import auto_convert_model, is_rknn_compatible try: import openvino as ov @@ -25,7 +27,33 @@ class ONNXModelRunner: self.model_path = model_path self.ort: ort.InferenceSession = None self.ov: ov.Core = None - providers, options = get_ort_providers(device == "CPU", device, requires_fp16) + self.rknn = None + self.type = "ort" + + try: + if device != "CPU" and is_rknn_compatible(model_path): + # Try to auto-convert to RKNN format + rknn_path = auto_convert_model(model_path) + if rknn_path: + try: + self.rknn = RKNNModelRunner(rknn_path, device) + self.type = "rknn" + logger.info(f"Using RKNN model: {rknn_path}") + return + except Exception as e: + logger.debug( + f"Failed to load RKNN model, falling back to ONNX: {e}" + ) + self.rknn = None + except ImportError: + pass + + # Fall back to standard ONNX providers + providers, options = get_ort_providers( + device == "CPU", + device, + requires_fp16, + ) self.interpreter = None if "OpenVINOExecutionProvider" in providers: @@ -55,7 +83,9 @@ class ONNXModelRunner: ) def get_input_names(self) -> list[str]: - if self.type == "ov": + if self.type == "rknn": + return self.rknn.get_input_names() + elif self.type == "ov": input_names = [] for input in self.interpreter.inputs: @@ -67,7 +97,9 @@ class ONNXModelRunner: def get_input_width(self): """Get the input width of the model regardless of backend.""" - if self.type == "ort": + if self.type == "rknn": + return self.rknn.get_input_width() + elif self.type == "ort": return self.ort.get_inputs()[0].shape[3] elif self.type == "ov": input_info = self.interpreter.inputs @@ -90,8 +122,10 @@ class ONNXModelRunner: return -1 return -1 - def run(self, input: dict[str, Any]) -> Any: - if self.type == "ov": + def run(self, input: dict[str, Any]) -> Any | None: + if self.type == "rknn": + return self.rknn.run(input) + elif self.type == "ov": infer_request = self.interpreter.create_infer_request() try: @@ -107,3 +141,103 @@ class ONNXModelRunner: return outputs elif self.type == "ort": return self.ort.run(None, input) + + +class RKNNModelRunner: + """Run RKNN models for embeddings.""" + + def __init__(self, model_path: str, device: str = "AUTO", model_type: str = None): + self.model_path = model_path + self.device = device + self.model_type = model_type + self.rknn = None + self._load_model() + + def _load_model(self): + """Load the RKNN model.""" + try: + from rknnlite.api import RKNNLite + + self.rknn = RKNNLite(verbose=False) + + if self.rknn.load_rknn(self.model_path) != 0: + logger.error(f"Failed to load RKNN model: {self.model_path}") + raise RuntimeError("Failed to load RKNN model") + + if self.rknn.init_runtime() != 0: + logger.error("Failed to initialize RKNN runtime") + raise RuntimeError("Failed to initialize RKNN runtime") + + logger.info(f"Successfully loaded RKNN model: {self.model_path}") + + except ImportError: + logger.error("RKNN Lite not available") + raise ImportError("RKNN Lite not available") + except Exception as e: + logger.error(f"Error loading RKNN model: {e}") + raise + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + # For CLIP models, we need to determine the model type from the path + model_name = os.path.basename(self.model_path).lower() + + if "vision" in model_name: + return ["pixel_values"] + elif "arcface" in model_name: + return ["data"] + else: + # Default fallback - try to infer from model type + if self.model_type and "jina-clip" in self.model_type: + if "vision" in self.model_type: + return ["pixel_values"] + + # Generic fallback + return ["input"] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + # For CLIP vision models, this is typically 224 + model_name = os.path.basename(self.model_path).lower() + if "vision" in model_name: + return 224 # CLIP V1 uses 224x224 + elif "arcface" in model_name: + return 112 + return -1 + + def run(self, inputs: dict[str, Any]) -> Any: + """Run inference with the RKNN model.""" + if not self.rknn: + raise RuntimeError("RKNN model not loaded") + + try: + input_names = self.get_input_names() + rknn_inputs = [] + + for name in input_names: + if name in inputs: + if name == "pixel_values": + # RKNN expects NHWC format, but ONNX typically provides NCHW + # Transpose from [batch, channels, height, width] to [batch, height, width, channels] + pixel_data = inputs[name] + if len(pixel_data.shape) == 4 and pixel_data.shape[1] == 3: + # Transpose from NCHW to NHWC + pixel_data = np.transpose(pixel_data, (0, 2, 3, 1)) + rknn_inputs.append(pixel_data) + else: + rknn_inputs.append(inputs[name]) + + outputs = self.rknn.inference(inputs=rknn_inputs) + return outputs + + except Exception as e: + logger.error(f"Error during RKNN inference: {e}") + raise + + def __del__(self): + """Cleanup when the runner is destroyed.""" + if self.rknn: + try: + self.rknn.release() + except Exception: + pass diff --git a/frigate/events/audio.py b/frigate/events/audio.py index cb1fe392b..31b9a7f3c 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -2,21 +2,15 @@ import datetime import logging -import random -import string import threading import time from multiprocessing.managers import DictProxy from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Tuple +from typing import Tuple import numpy as np from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum -from frigate.comms.event_metadata_updater import ( - EventMetadataPublisher, - EventMetadataTypeEnum, -) from frigate.comms.inter_process import InterProcessRequestor from frigate.config import CameraConfig, CameraInput, FfmpegConfig, FrigateConfig from frigate.config.camera.updater import ( @@ -29,7 +23,9 @@ from frigate.const import ( AUDIO_MAX_BIT_RANGE, AUDIO_MIN_CONFIDENCE, AUDIO_SAMPLE_RATE, + EXPIRE_AUDIO_ACTIVITY, PROCESS_PRIORITY_HIGH, + UPDATE_AUDIO_ACTIVITY, ) from frigate.data_processing.common.audio_transcription.model import ( AudioTranscriptionModelRunner, @@ -159,7 +155,6 @@ class AudioEventMaintainer(threading.Thread): self.config = config self.camera_config = camera self.camera_metrics = camera_metrics - self.detections: dict[dict[str, Any]] = {} self.stop_event = stop_event self.detector = AudioTfl(stop_event, self.camera_config.audio.num_threads) self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) @@ -184,7 +179,6 @@ class AudioEventMaintainer(threading.Thread): ], ) self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value) - self.event_metadata_publisher = EventMetadataPublisher() if self.camera_config.audio_transcription.enabled_in_config: # init the transcription processor for this camera @@ -216,12 +210,13 @@ class AudioEventMaintainer(threading.Thread): self.camera_metrics[self.camera_config.name].audio_rms.value = rms self.camera_metrics[self.camera_config.name].audio_dBFS.value = dBFS + audio_detections: list[Tuple[str, float]] = [] + # only run audio detection when volume is above min_volume if rms >= self.camera_config.audio.min_volume: # create waveform relative to max range and look for detections waveform = (audio / AUDIO_MAX_BIT_RANGE).astype(np.float32) model_detections = self.detector.detect(waveform) - audio_detections = [] for label, score, _ in model_detections: self.logger.debug( @@ -234,8 +229,7 @@ class AudioEventMaintainer(threading.Thread): if score > dict( (self.camera_config.audio.filters or {}).get(label, {}) ).get("threshold", 0.8): - self.handle_detection(label, score) - audio_detections.append(label) + audio_detections.append((label, score)) # send audio detection data self.detection_publisher.publish( @@ -243,10 +237,16 @@ class AudioEventMaintainer(threading.Thread): self.camera_config.name, datetime.datetime.now().timestamp(), dBFS, - audio_detections, + [label for label, _ in audio_detections], ) ) + # send audio activity update + self.requestor.send_data( + UPDATE_AUDIO_ACTIVITY, + {self.camera_config.name: {"detections": audio_detections}}, + ) + # run audio transcription if self.transcription_processor is not None: if self.camera_config.audio_transcription.live_enabled: @@ -261,8 +261,6 @@ class AudioEventMaintainer(threading.Thread): else: self.transcription_processor.check_unload_model() - self.expire_detections() - def calculate_audio_levels(self, audio_as_float: np.float32) -> Tuple[float, float]: # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude # Note: np.float32 isn't serializable, we must use np.float64 to publish the message @@ -279,75 +277,6 @@ class AudioEventMaintainer(threading.Thread): return float(rms), float(dBFS) - def handle_detection(self, label: str, score: float) -> None: - if self.detections.get(label): - self.detections[label]["last_detection"] = ( - datetime.datetime.now().timestamp() - ) - else: - now = datetime.datetime.now().timestamp() - rand_id = "".join( - random.choices(string.ascii_lowercase + string.digits, k=6) - ) - event_id = f"{now}-{rand_id}" - self.requestor.send_data(f"{self.camera_config.name}/audio/{label}", "ON") - - self.event_metadata_publisher.publish( - ( - now, - self.camera_config.name, - label, - event_id, - True, - score, - None, - None, - "audio", - {}, - ), - EventMetadataTypeEnum.manual_event_create.value, - ) - self.detections[label] = { - "id": event_id, - "label": label, - "last_detection": now, - } - - def expire_detections(self) -> None: - now = datetime.datetime.now().timestamp() - - for detection in self.detections.values(): - if not detection: - continue - - if ( - now - detection.get("last_detection", now) - > self.camera_config.audio.max_not_heard - ): - self.requestor.send_data( - f"{self.camera_config.name}/audio/{detection['label']}", "OFF" - ) - - self.event_metadata_publisher.publish( - (detection["id"], detection["last_detection"]), - EventMetadataTypeEnum.manual_event_end.value, - ) - self.detections[detection["label"]] = None - - def expire_all_detections(self) -> None: - """Immediately end all current detections""" - now = datetime.datetime.now().timestamp() - for label, detection in list(self.detections.items()): - if detection: - self.requestor.send_data( - f"{self.camera_config.name}/audio/{label}", "OFF" - ) - self.event_metadata_publisher.publish( - (detection["id"], now), - EventMetadataTypeEnum.manual_event_end.value, - ) - self.detections[label] = None - def start_or_restart_ffmpeg(self) -> None: self.audio_listener = start_or_restart_ffmpeg( self.ffmpeg_cmd, @@ -356,6 +285,7 @@ class AudioEventMaintainer(threading.Thread): self.chunk_size, self.audio_listener, ) + self.requestor.send_data(f"{self.camera_config.name}/status/audio", "online") def read_audio(self) -> None: def log_and_restart() -> None: @@ -371,6 +301,9 @@ class AudioEventMaintainer(threading.Thread): if not chunk: if self.audio_listener.poll() is not None: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "offline" + ) self.logger.error("ffmpeg process is not running, restarting...") log_and_restart() return @@ -396,10 +329,15 @@ class AudioEventMaintainer(threading.Thread): ) self.start_or_restart_ffmpeg() else: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "disabled" + ) self.logger.debug( f"Disabling audio detections for {self.camera_config.name}, ending events" ) - self.expire_all_detections() + self.requestor.send_data( + EXPIRE_AUDIO_ACTIVITY, self.camera_config.name + ) stop_ffmpeg(self.audio_listener, self.logger) self.audio_listener = None self.was_enabled = enabled diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index a26efae3e..d23c8b372 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -7,6 +7,7 @@ from typing import Any from frigate.const import ( FFMPEG_HVC1_ARGS, + FFMPEG_HWACCEL_AMF, FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_RKMPP, FFMPEG_HWACCEL_VAAPI, @@ -74,6 +75,7 @@ PRESETS_HW_ACCEL_DECODE = { f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra": "-hwaccel rkmpp -hwaccel_output_format drm_prime", # experimental presets FFMPEG_HWACCEL_VULKAN: "-hwaccel vulkan -init_hw_device vulkan=gpu:0 -filter_hw_device gpu -hwaccel_output_format vulkan", + FFMPEG_HWACCEL_AMF: "-hwaccel amf -init_hw_device amf=gpu:0 -filter_hw_device gpu -hwaccel_output_format amf", } PRESETS_HW_ACCEL_DECODE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_DECODE[ FFMPEG_HWACCEL_NVIDIA @@ -108,6 +110,7 @@ PRESETS_HW_ACCEL_SCALE = { "default": "-r {0} -vf fps={0},scale={1}:{2}", # experimental presets FFMPEG_HWACCEL_VULKAN: "-r {0} -vf fps={0},hwupload,scale_vulkan=w={1}:h={2},hwdownload", + FFMPEG_HWACCEL_AMF: "-r {0} -vf fps={0},hwupload,scale_amf=w={1}:h={2},hwdownload", } PRESETS_HW_ACCEL_SCALE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_SCALE[ FFMPEG_HWACCEL_NVIDIA @@ -133,6 +136,7 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile main {2}", FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -g 50 -profile:v high {2}", "default": "{0} -hide_banner {1} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {2}", } PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = ( @@ -161,6 +165,7 @@ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { "preset-jetson-h265": "{0} -hide_banner {1} -c:v hevc_nvmpi -profile main {2}", FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -profile:v high {2}", "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {2}", } PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = ( diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 4a9789097..46c3bd8a0 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -115,7 +115,7 @@ Sequence details: response = self._send(context_prompt, thumbnails) - if debug_save: + if debug_save and response: with open( os.path.join( CLIPS_DIR, "genai-requests", review_data["id"], "response.txt" diff --git a/frigate/mypy.ini b/frigate/mypy.ini index f99f31ac5..5bad10f49 100644 --- a/frigate/mypy.ini +++ b/frigate/mypy.ini @@ -53,6 +53,9 @@ ignore_errors = false [mypy-frigate.stats] ignore_errors = false +[mypy-frigate.track.*] +ignore_errors = false + [mypy-frigate.types] ignore_errors = false diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index 921f88b46..9f4965111 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -1,7 +1,10 @@ import datetime import logging import queue +import threading +import time from abc import ABC, abstractmethod +from collections import deque from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent @@ -34,7 +37,7 @@ class ObjectDetector(ABC): pass -class LocalObjectDetector(ObjectDetector): +class BaseLocalDetector(ObjectDetector): def __init__( self, detector_config: BaseDetectorConfig = None, @@ -56,6 +59,18 @@ class LocalObjectDetector(ObjectDetector): self.detect_api = create_detector(detector_config) + def _transform_input(self, tensor_input: np.ndarray) -> np.ndarray: + if self.input_transform: + tensor_input = np.transpose(tensor_input, self.input_transform) + + if self.dtype == InputDTypeEnum.float: + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + elif self.dtype == InputDTypeEnum.float_denorm: + tensor_input = tensor_input.astype(np.float32) + + return tensor_input + def detect(self, tensor_input: np.ndarray, threshold=0.4): detections = [] @@ -73,19 +88,22 @@ class LocalObjectDetector(ObjectDetector): self.fps.update() return detections + +class LocalObjectDetector(BaseLocalDetector): def detect_raw(self, tensor_input: np.ndarray): - if self.input_transform: - tensor_input = np.transpose(tensor_input, self.input_transform) - - if self.dtype == InputDTypeEnum.float: - tensor_input = tensor_input.astype(np.float32) - tensor_input /= 255 - elif self.dtype == InputDTypeEnum.float_denorm: - tensor_input = tensor_input.astype(np.float32) - + tensor_input = self._transform_input(tensor_input) return self.detect_api.detect_raw(tensor_input=tensor_input) +class AsyncLocalObjectDetector(BaseLocalDetector): + def async_send_input(self, tensor_input: np.ndarray, connection_id: str): + tensor_input = self._transform_input(tensor_input) + return self.detect_api.send_input(connection_id, tensor_input) + + def async_receive_output(self): + return self.detect_api.receive_output() + + class DetectorRunner(FrigateProcess): def __init__( self, @@ -160,6 +178,110 @@ class DetectorRunner(FrigateProcess): logger.info("Exited detection process...") +class AsyncDetectorRunner(FrigateProcess): + def __init__( + self, + name, + detection_queue: Queue, + cameras: list[str], + avg_speed: Value, + start_time: Value, + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ) -> None: + super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True) + self.detection_queue = detection_queue + self.cameras = cameras + self.avg_speed = avg_speed + self.start_time = start_time + self.config = config + self.detector_config = detector_config + self.outputs: dict = {} + self._frame_manager: SharedMemoryFrameManager | None = None + self._publisher: ObjectDetectorPublisher | None = None + self._detector: AsyncLocalObjectDetector | None = None + self.send_times = deque() + + def create_output_shm(self, name: str): + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) + self.outputs[name] = {"shm": out_shm, "np": out_np} + + def _detect_worker(self) -> None: + logger.info("Starting Detect Worker Thread") + while not self.stop_event.is_set(): + try: + connection_id = self.detection_queue.get(timeout=1) + except queue.Empty: + continue + + input_frame = self._frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, + self.detector_config.model.width, + 3, + ), + ) + + if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") + continue + + # mark start time and send to accelerator + self.send_times.append(time.perf_counter()) + self._detector.async_send_input(input_frame, connection_id) + + def _result_worker(self) -> None: + logger.info("Starting Result Worker Thread") + while not self.stop_event.is_set(): + connection_id, detections = self._detector.async_receive_output() + + if not self.send_times: + # guard; shouldn't happen if send/recv are balanced + continue + ts = self.send_times.popleft() + duration = time.perf_counter() - ts + + # release input buffer + self._frame_manager.close(connection_id) + + if connection_id not in self.outputs: + self.create_output_shm(connection_id) + + # write results and publish + if detections is not None: + self.outputs[connection_id]["np"][:] = detections[:] + self._publisher.publish(connection_id) + + # update timers + self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10 + self.start_time.value = 0.0 + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + self._frame_manager = SharedMemoryFrameManager() + self._publisher = ObjectDetectorPublisher() + self._detector = AsyncLocalObjectDetector(detector_config=self.detector_config) + + for name in self.cameras: + self.create_output_shm(name) + + t_detect = threading.Thread(target=self._detect_worker, daemon=True) + t_result = threading.Thread(target=self._result_worker, daemon=True) + t_detect.start() + t_result.start() + + while not self.stop_event.is_set(): + time.sleep(0.5) + + self._publisher.stop() + logger.info("Exited async detection process...") + + class ObjectDetectProcess: def __init__( self, @@ -198,16 +320,30 @@ class ObjectDetectProcess: self.detection_start.value = 0.0 if (self.detect_process is not None) and self.detect_process.is_alive(): self.stop() - self.detect_process = DetectorRunner( - f"frigate.detector:{self.name}", - self.detection_queue, - self.cameras, - self.avg_inference_speed, - self.detection_start, - self.config, - self.detector_config, - self.stop_event, - ) + + # Async path for MemryX + if self.detector_config.type == "memryx": + self.detect_process = AsyncDetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) + else: + self.detect_process = DetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) self.detect_process.start() diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index f0d8419dd..beecc62ab 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -60,10 +60,10 @@ class PtzMotionEstimator: def motion_estimator( self, - detections: list[dict[str, Any]], + detections: list[tuple[Any, Any, Any, Any, Any, Any]], frame_name: str, frame_time: float, - camera: str, + camera: str | None, ): # If we've just started up or returned to our preset, reset motion estimator for new tracking session if self.ptz_metrics.reset.is_set(): diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 9d1e28306..b0359e71d 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -308,7 +308,12 @@ class RecordingCleanup(threading.Thread): now - datetime.timedelta(days=config.record.continuous.days) ).timestamp() motion_expire_date = ( - now - datetime.timedelta(days=config.record.motion.days) + now + - datetime.timedelta( + days=max( + config.record.motion.days, config.record.continuous.days + ) # can't keep motion for less than continuous + ) ).timestamp() # Get all the reviews to check against diff --git a/frigate/stats/util.py b/frigate/stats/util.py index ee93bb6e6..6d20f8f9a 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -8,7 +8,6 @@ from json import JSONDecodeError from multiprocessing.managers import DictProxy from typing import Any, Optional -import psutil import requests from requests.exceptions import RequestException @@ -18,9 +17,11 @@ from frigate.data_processing.types import DataProcessorMetrics from frigate.object_detection.base import ObjectDetectProcess from frigate.types import StatsTrackingTypes from frigate.util.services import ( + calculate_shm_requirements, get_amd_gpu_stats, get_bandwidth_stats, get_cpu_stats, + get_fs_type, get_intel_gpu_stats, get_jetson_stats, get_nvidia_gpu_stats, @@ -70,16 +71,6 @@ def stats_init( return stats_tracking -def get_fs_type(path: str) -> str: - bestMatch = "" - fsType = "" - for part in psutil.disk_partitions(all=True): - if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint): - fsType = part.fstype - bestMatch = part.mountpoint - return fsType - - def read_temperature(path: str) -> Optional[float]: if os.path.isfile(path): with open(path) as f: @@ -389,7 +380,7 @@ def stats_snapshot( "last_updated": int(time.time()), } - for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: + for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]: try: storage_stats = shutil.disk_usage(path) except (FileNotFoundError, OSError): @@ -403,6 +394,8 @@ def stats_snapshot( "mount_type": get_fs_type(path), } + stats["service"]["storage"]["/dev/shm"] = calculate_shm_requirements(config) + stats["processes"] = {} for name, pid in stats_tracking["processes"].items(): stats["processes"][name] = { diff --git a/frigate/track/__init__.py b/frigate/track/__init__.py index dc72be4f0..b5453aaeb 100644 --- a/frigate/track/__init__.py +++ b/frigate/track/__init__.py @@ -11,6 +11,9 @@ class ObjectTracker(ABC): @abstractmethod def match_and_update( - self, frame_name: str, frame_time: float, detections: list[dict[str, Any]] + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], ) -> None: pass diff --git a/frigate/track/centroid_tracker.py b/frigate/track/centroid_tracker.py index 25d4cb860..56f20629c 100644 --- a/frigate/track/centroid_tracker.py +++ b/frigate/track/centroid_tracker.py @@ -1,25 +1,26 @@ import random import string from collections import defaultdict +from typing import Any import numpy as np from scipy.spatial import distance as dist from frigate.config import DetectConfig from frigate.track import ObjectTracker -from frigate.util import intersection_over_union +from frigate.util.image import intersection_over_union class CentroidTracker(ObjectTracker): def __init__(self, config: DetectConfig): - self.tracked_objects = {} - self.untracked_object_boxes = [] - self.disappeared = {} - self.positions = {} + self.tracked_objects: dict[str, dict[str, Any]] = {} + self.untracked_object_boxes: list[tuple[int, int, int, int]] = [] + self.disappeared: dict[str, Any] = {} + self.positions: dict[str, Any] = {} self.max_disappeared = config.max_disappeared self.detect_config = config - def register(self, index, obj): + def register(self, obj: dict[str, Any]) -> None: rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) id = f"{obj['frame_time']}-{rand_id}" obj["id"] = id @@ -39,13 +40,13 @@ class CentroidTracker(ObjectTracker): "ymax": self.detect_config.height, } - def deregister(self, id): + def deregister(self, id: str) -> None: del self.tracked_objects[id] del self.disappeared[id] # tracks the current position of the object based on the last N bounding boxes # returns False if the object has moved outside its previous position - def update_position(self, id, box): + def update_position(self, id: str, box: tuple[int, int, int, int]) -> bool: position = self.positions[id] position_box = ( position["xmin"], @@ -88,7 +89,7 @@ class CentroidTracker(ObjectTracker): return True - def is_expired(self, id): + def is_expired(self, id: str) -> bool: obj = self.tracked_objects[id] # get the max frames for this label type or the default max_frames = self.detect_config.stationary.max_frames.objects.get( @@ -108,7 +109,7 @@ class CentroidTracker(ObjectTracker): return False - def update(self, id, new_obj): + def update(self, id: str, new_obj: dict[str, Any]) -> None: self.disappeared[id] = 0 # update the motionless count if the object has not moved to a new position if self.update_position(id, new_obj["box"]): @@ -129,25 +130,30 @@ class CentroidTracker(ObjectTracker): self.tracked_objects[id].update(new_obj) - def update_frame_times(self, frame_name, frame_time): + def update_frame_times(self, frame_name: str, frame_time: float) -> None: for id in list(self.tracked_objects.keys()): self.tracked_objects[id]["frame_time"] = frame_time self.tracked_objects[id]["motionless_count"] += 1 if self.is_expired(id): self.deregister(id) - def match_and_update(self, frame_time, detections): + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: # group by name detection_groups = defaultdict(lambda: []) - for obj in detections: - detection_groups[obj[0]].append( + for det in detections: + detection_groups[det[0]].append( { - "label": obj[0], - "score": obj[1], - "box": obj[2], - "area": obj[3], - "ratio": obj[4], - "region": obj[5], + "label": det[0], + "score": det[1], + "box": det[2], + "area": det[3], + "ratio": det[4], + "region": det[5], "frame_time": frame_time, } ) @@ -180,7 +186,7 @@ class CentroidTracker(ObjectTracker): if len(current_objects) == 0: for index, obj in enumerate(group): - self.register(index, obj) + self.register(obj) continue new_centroids = np.array([o["centroid"] for o in group]) @@ -238,4 +244,4 @@ class CentroidTracker(ObjectTracker): # register each new input centroid as a trackable object else: for col in unusedCols: - self.register(col, group[col]) + self.register(group[col]) diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 900971e0d..cdbd35f10 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -5,14 +5,10 @@ from typing import Any, Sequence import cv2 import numpy as np -from norfair import ( - Detection, - Drawable, - OptimizedKalmanFilterFactory, - Tracker, - draw_boxes, -) -from norfair.drawing.drawer import Drawer +from norfair.drawing.draw_boxes import draw_boxes +from norfair.drawing.drawer import Drawable, Drawer +from norfair.filter import OptimizedKalmanFilterFactory +from norfair.tracker import Detection, TrackedObject, Tracker from rich import print from rich.console import Console from rich.table import Table @@ -43,7 +39,7 @@ MAX_STATIONARY_HISTORY = 10 # - could be variable based on time since last_detection # - include estimated velocity in the distance (car driving by of a parked car) # - include some visual similarity factor in the distance for occlusions -def distance(detection: np.array, estimate: np.array) -> float: +def distance(detection: np.ndarray, estimate: np.ndarray) -> float: # ultimately, this should try and estimate distance in 3-dimensional space # consider change in location, width, and height @@ -73,14 +69,16 @@ def distance(detection: np.array, estimate: np.array) -> float: change = np.append(distance, np.array([width_ratio, height_ratio])) # calculate euclidean distance of the change vector - return np.linalg.norm(change) + return float(np.linalg.norm(change)) -def frigate_distance(detection: Detection, tracked_object) -> float: +def frigate_distance(detection: Detection, tracked_object: TrackedObject) -> float: return distance(detection.points, tracked_object.estimate) -def histogram_distance(matched_not_init_trackers, unmatched_trackers): +def histogram_distance( + matched_not_init_trackers: TrackedObject, unmatched_trackers: TrackedObject +) -> float: snd_embedding = unmatched_trackers.last_detection.embedding if snd_embedding is None: @@ -110,17 +108,17 @@ class NorfairTracker(ObjectTracker): ptz_metrics: PTZMetrics, ): self.frame_manager = SharedMemoryFrameManager() - self.tracked_objects = {} + self.tracked_objects: dict[str, dict[str, Any]] = {} self.untracked_object_boxes: list[list[int]] = [] - self.disappeared = {} - self.positions = {} - self.stationary_box_history: dict[str, list[list[int, int, int, int]]] = {} + self.disappeared: dict[str, int] = {} + self.positions: dict[str, dict[str, Any]] = {} + self.stationary_box_history: dict[str, list[list[int]]] = {} self.camera_config = config self.detect_config = config.detect self.ptz_metrics = ptz_metrics - self.ptz_motion_estimator = {} + self.ptz_motion_estimator: PtzMotionEstimator | None = None self.camera_name = config.name - self.track_id_map = {} + self.track_id_map: dict[str, str] = {} # Define tracker configurations for static camera self.object_type_configs = { @@ -169,7 +167,7 @@ class NorfairTracker(ObjectTracker): "distance_threshold": 3, } - self.trackers = {} + self.trackers: dict[str, dict[str, Tracker]] = {} # Handle static trackers for obj_type, tracker_config in self.object_type_configs.items(): if obj_type in self.camera_config.objects.track: @@ -195,19 +193,21 @@ class NorfairTracker(ObjectTracker): self.default_tracker = { "static": Tracker( distance_function=frigate_distance, - distance_threshold=self.default_tracker_config["distance_threshold"], + distance_threshold=self.default_tracker_config[ # type: ignore[arg-type] + "distance_threshold" + ], initialization_delay=self.detect_config.min_initialized, - hit_counter_max=self.detect_config.max_disappeared, - filter_factory=self.default_tracker_config["filter_factory"], + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_tracker_config["filter_factory"], # type: ignore[arg-type] ), "ptz": Tracker( distance_function=frigate_distance, distance_threshold=self.default_ptz_tracker_config[ "distance_threshold" - ], + ], # type: ignore[arg-type] initialization_delay=self.detect_config.min_initialized, - hit_counter_max=self.detect_config.max_disappeared, - filter_factory=self.default_ptz_tracker_config["filter_factory"], + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_ptz_tracker_config["filter_factory"], # type: ignore[arg-type] ), } @@ -216,7 +216,7 @@ class NorfairTracker(ObjectTracker): self.camera_config, self.ptz_metrics ) - def _create_tracker(self, obj_type, tracker_config): + def _create_tracker(self, obj_type: str, tracker_config: dict[str, Any]) -> Tracker: """Helper function to create a tracker with given configuration.""" tracker_params = { "distance_function": tracker_config["distance_function"], @@ -258,7 +258,7 @@ class NorfairTracker(ObjectTracker): return self.trackers[object_type][mode] return self.default_tracker[mode] - def register(self, track_id, obj): + def register(self, track_id: str, obj: dict[str, Any]) -> None: rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) id = f"{obj['frame_time']}-{rand_id}" self.track_id_map[track_id] = id @@ -270,7 +270,7 @@ class NorfairTracker(ObjectTracker): # Get the correct tracker for this object's label tracker = self.get_tracker(obj["label"]) obj_match = next( - (o for o in tracker.tracked_objects if o.global_id == track_id), None + (o for o in tracker.tracked_objects if str(o.global_id) == track_id), None ) # if we don't have a match, we have a new object obj["score_history"] = ( @@ -297,7 +297,7 @@ class NorfairTracker(ObjectTracker): } self.stationary_box_history[id] = boxes - def deregister(self, id, track_id): + def deregister(self, id: str, track_id: str) -> None: obj = self.tracked_objects[id] del self.tracked_objects[id] @@ -314,14 +314,14 @@ class NorfairTracker(ObjectTracker): tracker.tracked_objects = [ o for o in tracker.tracked_objects - if o.global_id != track_id and o.hit_counter < 0 + if str(o.global_id) != track_id and o.hit_counter < 0 ] del self.track_id_map[track_id] # tracks the current position of the object based on the last N bounding boxes # returns False if the object has moved outside its previous position - def update_position(self, id: str, box: list[int, int, int, int], stationary: bool): + def update_position(self, id: str, box: list[int], stationary: bool) -> bool: xmin, ymin, xmax, ymax = box position = self.positions[id] self.stationary_box_history[id].append(box) @@ -396,7 +396,7 @@ class NorfairTracker(ObjectTracker): return True - def is_expired(self, id): + def is_expired(self, id: str) -> bool: obj = self.tracked_objects[id] # get the max frames for this label type or the default max_frames = self.detect_config.stationary.max_frames.objects.get( @@ -416,7 +416,7 @@ class NorfairTracker(ObjectTracker): return False - def update(self, track_id, obj): + def update(self, track_id: str, obj: dict[str, Any]) -> None: id = self.track_id_map[track_id] self.disappeared[id] = 0 stationary = ( @@ -443,7 +443,7 @@ class NorfairTracker(ObjectTracker): self.tracked_objects[id].update(obj) - def update_frame_times(self, frame_name: str, frame_time: float): + def update_frame_times(self, frame_name: str, frame_time: float) -> None: # if the object was there in the last frame, assume it's still there detections = [ ( @@ -460,10 +460,13 @@ class NorfairTracker(ObjectTracker): self.match_and_update(frame_name, frame_time, detections=detections) def match_and_update( - self, frame_name: str, frame_time: float, detections: list[dict[str, Any]] - ): + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: # Group detections by object type - detections_by_type = {} + detections_by_type: dict[str, list[Detection]] = {} for obj in detections: label = obj[0] if label not in detections_by_type: @@ -551,28 +554,28 @@ class NorfairTracker(ObjectTracker): estimate = ( max(0, estimate[0]), max(0, estimate[1]), - min(self.detect_config.width - 1, estimate[2]), - min(self.detect_config.height - 1, estimate[3]), + min(self.detect_config.width - 1, estimate[2]), # type: ignore[operator] + min(self.detect_config.height - 1, estimate[3]), # type: ignore[operator] ) - obj = { + new_obj = { **t.last_detection.data, "estimate": estimate, "estimate_velocity": t.estimate_velocity, } - active_ids.append(t.global_id) - if t.global_id not in self.track_id_map: - self.register(t.global_id, obj) + active_ids.append(str(t.global_id)) + if str(t.global_id) not in self.track_id_map: + self.register(str(t.global_id), new_obj) # if there wasn't a detection in this frame, increment disappeared elif t.last_detection.data["frame_time"] != frame_time: - id = self.track_id_map[t.global_id] + id = self.track_id_map[str(t.global_id)] self.disappeared[id] += 1 # sometimes the estimate gets way off # only update if the upper left corner is actually upper left if estimate[0] < estimate[2] and estimate[1] < estimate[3]: - self.tracked_objects[id]["estimate"] = obj["estimate"] + self.tracked_objects[id]["estimate"] = new_obj["estimate"] # else update it else: - self.update(t.global_id, obj) + self.update(str(t.global_id), new_obj) # clear expired tracks expired_ids = [k for k in self.track_id_map.keys() if k not in active_ids] @@ -585,7 +588,7 @@ class NorfairTracker(ObjectTracker): o[2] for o in detections if o[2] not in tracked_object_boxes ] - def print_objects_as_table(self, tracked_objects: Sequence): + def print_objects_as_table(self, tracked_objects: Sequence) -> None: """Used for helping in debugging""" print() console = Console() @@ -605,13 +608,13 @@ class NorfairTracker(ObjectTracker): ) console.print(table) - def debug_draw(self, frame, frame_time): + def debug_draw(self, frame: np.ndarray, frame_time: float) -> None: # Collect all tracked objects from each tracker - all_tracked_objects = [] + all_tracked_objects: list[TrackedObject] = [] # print a table to the console with norfair tracked object info if False: - if len(self.trackers["license_plate"]["static"].tracked_objects) > 0: + if len(self.trackers["license_plate"]["static"].tracked_objects) > 0: # type: ignore[unreachable] self.print_objects_as_table( self.trackers["license_plate"]["static"].tracked_objects ) @@ -638,9 +641,9 @@ class NorfairTracker(ObjectTracker): # draw the estimated bounding box draw_boxes(frame, all_tracked_objects, color="green", draw_ids=True) # draw the detections that were detected in the current frame - draw_boxes(frame, active_detections, color="blue", draw_ids=True) + draw_boxes(frame, active_detections, color="blue", draw_ids=True) # type: ignore[arg-type] # draw the detections that are missing in the current frame - draw_boxes(frame, missing_detections, color="red", draw_ids=True) + draw_boxes(frame, missing_detections, color="red", draw_ids=True) # type: ignore[arg-type] # draw the distance calculation for the last detection # estimate vs detection @@ -648,8 +651,8 @@ class NorfairTracker(ObjectTracker): ld = obj.last_detection # bottom right text_anchor = ( - ld.points[1, 0], - ld.points[1, 1], + ld.points[1, 0], # type: ignore[index] + ld.points[1, 1], # type: ignore[index] ) frame = Drawer.text( frame, @@ -662,7 +665,7 @@ class NorfairTracker(ObjectTracker): if False: # draw the current formatted time on the frame - from datetime import datetime + from datetime import datetime # type: ignore[unreachable] formatted_time = datetime.fromtimestamp(frame_time).strftime( "%m/%d/%Y %I:%M:%S %p" diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index 5ffd14446..25128d6df 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -6,6 +6,7 @@ import queue import threading from collections import defaultdict from enum import Enum +from multiprocessing import Queue as MpQueue from multiprocessing.synchronize import Event as MpEvent from typing import Any @@ -39,6 +40,7 @@ from frigate.const import ( ) from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.models import Event, ReviewSegment, Timeline +from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.track.tracked_object import TrackedObject from frigate.util.image import SharedMemoryFrameManager @@ -56,10 +58,10 @@ class TrackedObjectProcessor(threading.Thread): self, config: FrigateConfig, dispatcher: Dispatcher, - tracked_objects_queue, - ptz_autotracker_thread, - stop_event, - ): + tracked_objects_queue: MpQueue, + ptz_autotracker_thread: PtzAutoTrackerThread, + stop_event: MpEvent, + ) -> None: super().__init__(name="detected_frames_processor") self.config = config self.dispatcher = dispatcher @@ -98,8 +100,12 @@ class TrackedObjectProcessor(threading.Thread): # } # } # } - self.zone_data = defaultdict(lambda: defaultdict(dict)) - self.active_zone_data = defaultdict(lambda: defaultdict(dict)) + self.zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) + self.active_zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) for camera in self.config.cameras.keys(): self.create_camera_state(camera) @@ -107,7 +113,7 @@ class TrackedObjectProcessor(threading.Thread): def create_camera_state(self, camera: str) -> None: """Creates a new camera state.""" - def start(camera: str, obj: TrackedObject, frame_name: str): + def start(camera: str, obj: TrackedObject, frame_name: str) -> None: self.event_sender.publish( ( EventTypeEnum.tracked_object, @@ -118,7 +124,7 @@ class TrackedObjectProcessor(threading.Thread): ) ) - def update(camera: str, obj: TrackedObject, frame_name: str): + def update(camera: str, obj: TrackedObject, frame_name: str) -> None: obj.has_snapshot = self.should_save_snapshot(camera, obj) obj.has_clip = self.should_retain_recording(camera, obj) after = obj.to_dict() @@ -139,10 +145,10 @@ class TrackedObjectProcessor(threading.Thread): ) ) - def autotrack(camera: str, obj: TrackedObject, frame_name: str): + def autotrack(camera: str, obj: TrackedObject, frame_name: str) -> None: self.ptz_autotracker_thread.ptz_autotracker.autotrack_object(camera, obj) - def end(camera: str, obj: TrackedObject, frame_name: str): + def end(camera: str, obj: TrackedObject, frame_name: str) -> None: # populate has_snapshot obj.has_snapshot = self.should_save_snapshot(camera, obj) obj.has_clip = self.should_retain_recording(camera, obj) @@ -211,7 +217,7 @@ class TrackedObjectProcessor(threading.Thread): return False - def camera_activity(camera, activity): + def camera_activity(camera: str, activity: dict[str, Any]) -> None: last_activity = self.camera_activity.get(camera) if not last_activity or activity != last_activity: @@ -229,7 +235,7 @@ class TrackedObjectProcessor(threading.Thread): camera_state.on("camera_activity", camera_activity) self.camera_states[camera] = camera_state - def should_save_snapshot(self, camera, obj: TrackedObject): + def should_save_snapshot(self, camera: str, obj: TrackedObject) -> bool: if obj.false_positive: return False @@ -252,7 +258,7 @@ class TrackedObjectProcessor(threading.Thread): return True - def should_retain_recording(self, camera: str, obj: TrackedObject): + def should_retain_recording(self, camera: str, obj: TrackedObject) -> bool: if obj.false_positive: return False @@ -272,7 +278,7 @@ class TrackedObjectProcessor(threading.Thread): return True - def should_mqtt_snapshot(self, camera, obj: TrackedObject): + def should_mqtt_snapshot(self, camera: str, obj: TrackedObject) -> bool: # object never changed position if obj.is_stationary(): return False @@ -287,7 +293,9 @@ class TrackedObjectProcessor(threading.Thread): return True - def update_mqtt_motion(self, camera, frame_time, motion_boxes): + def update_mqtt_motion( + self, camera: str, frame_time: float, motion_boxes: list + ) -> None: # publish if motion is currently being detected if motion_boxes: # only send ON if motion isn't already active @@ -313,11 +321,15 @@ class TrackedObjectProcessor(threading.Thread): # reset the last_motion so redundant `off` commands aren't sent self.last_motion_detected[camera] = 0 - def get_best(self, camera, label): + def get_best(self, camera: str, label: str) -> dict[str, Any]: # TODO: need a lock here camera_state = self.camera_states[camera] if label in camera_state.best_objects: best_obj = camera_state.best_objects[label] + + if not best_obj.thumbnail_data: + return {} + best = best_obj.thumbnail_data.copy() best["frame"] = camera_state.frame_cache.get( best_obj.thumbnail_data["frame_time"] @@ -340,7 +352,7 @@ class TrackedObjectProcessor(threading.Thread): return self.camera_states[camera].get_current_frame(draw_options) - def get_current_frame_time(self, camera) -> int: + def get_current_frame_time(self, camera: str) -> float: """Returns the latest frame time for a given camera.""" return self.camera_states[camera].current_frame_time @@ -348,7 +360,7 @@ class TrackedObjectProcessor(threading.Thread): self, event_id: str, sub_label: str | None, score: float | None ) -> None: """Update sub label for given event id.""" - tracked_obj: TrackedObject = None + tracked_obj: TrackedObject | None = None for state in self.camera_states.values(): tracked_obj = state.tracked_objects.get(event_id) @@ -357,7 +369,7 @@ class TrackedObjectProcessor(threading.Thread): break try: - event: Event = Event.get(Event.id == event_id) + event: Event | None = Event.get(Event.id == event_id) except DoesNotExist: event = None @@ -368,12 +380,12 @@ class TrackedObjectProcessor(threading.Thread): tracked_obj.obj_data["sub_label"] = (sub_label, score) if event: - event.sub_label = sub_label + event.sub_label = sub_label # type: ignore[assignment] data = event.data if sub_label is None: - data["sub_label_score"] = None + data["sub_label_score"] = None # type: ignore[index] elif score is not None: - data["sub_label_score"] = score + data["sub_label_score"] = score # type: ignore[index] event.data = data event.save() @@ -402,7 +414,7 @@ class TrackedObjectProcessor(threading.Thread): objects_list = [] sub_labels = set() events = Event.select(Event.id, Event.label, Event.sub_label).where( - Event.id.in_(detection_ids) + Event.id.in_(detection_ids) # type: ignore[call-arg, misc] ) for det_event in events: if det_event.sub_label: @@ -431,13 +443,11 @@ class TrackedObjectProcessor(threading.Thread): f"Updated sub_label for event {event_id} in review segment {review_segment.id}" ) - except ReviewSegment.DoesNotExist: + except DoesNotExist: logger.debug( f"No review segment found with event ID {event_id} when updating sub_label" ) - return True - def set_object_attribute( self, event_id: str, @@ -446,7 +456,7 @@ class TrackedObjectProcessor(threading.Thread): score: float | None, ) -> None: """Update attribute for given event id.""" - tracked_obj: TrackedObject = None + tracked_obj: TrackedObject | None = None for state in self.camera_states.values(): tracked_obj = state.tracked_objects.get(event_id) @@ -455,7 +465,7 @@ class TrackedObjectProcessor(threading.Thread): break try: - event: Event = Event.get(Event.id == event_id) + event: Event | None = Event.get(Event.id == event_id) except DoesNotExist: event = None @@ -470,16 +480,14 @@ class TrackedObjectProcessor(threading.Thread): if event: data = event.data - data[field_name] = field_value + data[field_name] = field_value # type: ignore[index] if field_value is None: - data[f"{field_name}_score"] = None + data[f"{field_name}_score"] = None # type: ignore[index] elif score is not None: - data[f"{field_name}_score"] = score + data[f"{field_name}_score"] = score # type: ignore[index] event.data = data event.save() - return True - def save_lpr_snapshot(self, payload: tuple) -> None: # save the snapshot image (frame, event_id, camera) = payload @@ -638,7 +646,7 @@ class TrackedObjectProcessor(threading.Thread): ) self.ongoing_manual_events.pop(event_id) - def force_end_all_events(self, camera: str, camera_state: CameraState): + def force_end_all_events(self, camera: str, camera_state: CameraState) -> None: """Ends all active events on camera when disabling.""" last_frame_name = camera_state.previous_frame_id for obj_id, obj in list(camera_state.tracked_objects.items()): @@ -656,7 +664,7 @@ class TrackedObjectProcessor(threading.Thread): {"enabled": False, "motion": 0, "objects": []}, ) - def run(self): + def run(self) -> None: while not self.stop_event.is_set(): # check for config updates updated_topics = self.camera_config_subscriber.check_for_updates() @@ -698,11 +706,14 @@ class TrackedObjectProcessor(threading.Thread): # check for sub label updates while True: - (raw_topic, payload) = self.sub_label_subscriber.check_for_update( - timeout=0 - ) + update = self.sub_label_subscriber.check_for_update(timeout=0) - if not raw_topic: + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: break topic = str(raw_topic) diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index afe85228e..111dd2c40 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -5,18 +5,19 @@ import math import os from collections import defaultdict from statistics import median -from typing import Any, Optional +from typing import Any, Optional, cast import cv2 import numpy as np from frigate.config import ( CameraConfig, - ModelConfig, + FilterConfig, SnapshotsConfig, UIConfig, ) from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.detectors.detector_config import ModelConfig from frigate.review.types import SeverityEnum from frigate.util.builtin import sanitize_float from frigate.util.image import ( @@ -46,11 +47,11 @@ class TrackedObject: model_config: ModelConfig, camera_config: CameraConfig, ui_config: UIConfig, - frame_cache, + frame_cache: dict[float, dict[str, Any]], obj_data: dict[str, Any], - ): + ) -> None: # set the score history then remove as it is not part of object state - self.score_history = obj_data["score_history"] + self.score_history: list[float] = obj_data["score_history"] del obj_data["score_history"] self.obj_data = obj_data @@ -61,24 +62,24 @@ class TrackedObject: self.frame_cache = frame_cache self.zone_presence: dict[str, int] = {} self.zone_loitering: dict[str, int] = {} - self.current_zones = [] - self.entered_zones = [] - self.attributes = defaultdict(float) + self.current_zones: list[str] = [] + self.entered_zones: list[str] = [] + self.attributes: dict[str, float] = defaultdict(float) self.false_positive = True self.has_clip = False self.has_snapshot = False self.top_score = self.computed_score = 0.0 - self.thumbnail_data = None + self.thumbnail_data: dict[str, Any] | None = None self.last_updated = 0 self.last_published = 0 self.frame = None self.active = True self.pending_loitering = False - self.speed_history = [] - self.current_estimated_speed = 0 - self.average_estimated_speed = 0 + self.speed_history: list[float] = [] + self.current_estimated_speed: float = 0 + self.average_estimated_speed: float = 0 self.velocity_angle = 0 - self.path_data = [] + self.path_data: list[tuple[Any, float]] = [] self.previous = self.to_dict() @property @@ -111,7 +112,7 @@ class TrackedObject: return None - def _is_false_positive(self): + def _is_false_positive(self) -> bool: # once a true positive, always a true positive if not self.false_positive: return False @@ -119,11 +120,13 @@ class TrackedObject: threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold return self.computed_score < threshold - def compute_score(self): + def compute_score(self) -> float: """get median of scores for object.""" return median(self.score_history) - def update(self, current_frame_time: float, obj_data, has_valid_frame: bool): + def update( + self, current_frame_time: float, obj_data: dict[str, Any], has_valid_frame: bool + ) -> tuple[bool, bool, bool, bool]: thumb_update = False significant_change = False path_update = False @@ -305,7 +308,7 @@ class TrackedObject: k: self.attributes[k] for k in self.logos if k in self.attributes } if len(recognized_logos) > 0: - max_logo = max(recognized_logos, key=recognized_logos.get) + max_logo = max(recognized_logos, key=recognized_logos.get) # type: ignore[arg-type] # don't overwrite sub label if it is already set if ( @@ -342,28 +345,30 @@ class TrackedObject: # update path width = self.camera_config.detect.width height = self.camera_config.detect.height - bottom_center = ( - round(obj_data["centroid"][0] / width, 4), - round(obj_data["box"][3] / height, 4), - ) - # calculate a reasonable movement threshold (e.g., 5% of the frame diagonal) - threshold = 0.05 * math.sqrt(width**2 + height**2) / max(width, height) - - if not self.path_data: - self.path_data.append((bottom_center, obj_data["frame_time"])) - path_update = True - elif ( - math.dist(self.path_data[-1][0], bottom_center) >= threshold - or len(self.path_data) == 1 - ): - # check Euclidean distance before appending - self.path_data.append((bottom_center, obj_data["frame_time"])) - path_update = True - logger.debug( - f"Point tracking: {obj_data['id']}, {bottom_center}, {obj_data['frame_time']}" + if width is not None and height is not None: + bottom_center = ( + round(obj_data["centroid"][0] / width, 4), + round(obj_data["box"][3] / height, 4), ) + # calculate a reasonable movement threshold (e.g., 5% of the frame diagonal) + threshold = 0.05 * math.sqrt(width**2 + height**2) / max(width, height) + + if not self.path_data: + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + elif ( + math.dist(self.path_data[-1][0], bottom_center) >= threshold + or len(self.path_data) == 1 + ): + # check Euclidean distance before appending + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + logger.debug( + f"Point tracking: {obj_data['id']}, {bottom_center}, {obj_data['frame_time']}" + ) + self.obj_data.update(obj_data) self.current_zones = current_zones logger.debug( @@ -371,7 +376,7 @@ class TrackedObject: ) return (thumb_update, significant_change, path_update, autotracker_update) - def to_dict(self): + def to_dict(self) -> dict[str, Any]: event = { "id": self.obj_data["id"], "camera": self.camera_config.name, @@ -413,10 +418,8 @@ class TrackedObject: return not self.is_stationary() def is_stationary(self) -> bool: - return ( - self.obj_data["motionless_count"] - > self.camera_config.detect.stationary.threshold - ) + count = cast(int | float, self.obj_data["motionless_count"]) + return count > (self.camera_config.detect.stationary.threshold or 50) def get_thumbnail(self, ext: str) -> bytes | None: img_bytes = self.get_img_bytes( @@ -453,9 +456,9 @@ class TrackedObject: def get_img_bytes( self, ext: str, - timestamp=False, - bounding_box=False, - crop=False, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, height: int | None = None, quality: int | None = None, ) -> bytes | None: @@ -532,18 +535,18 @@ class TrackedObject: best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA ) if timestamp: - color = self.camera_config.timestamp_style.color + colors = self.camera_config.timestamp_style.color draw_timestamp( best_frame, self.thumbnail_data["frame_time"], self.camera_config.timestamp_style.format, font_effect=self.camera_config.timestamp_style.effect, font_thickness=self.camera_config.timestamp_style.thickness, - font_color=(color.blue, color.green, color.red), + font_color=(colors.blue, colors.green, colors.red), position=self.camera_config.timestamp_style.position, ) - quality_params = None + quality_params = [] if ext == "jpg": quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality or 70] @@ -596,6 +599,9 @@ class TrackedObject: p.write(png_bytes) def write_thumbnail_to_disk(self) -> None: + if not self.camera_config.name: + return + directory = os.path.join(THUMB_DIR, self.camera_config.name) if not os.path.exists(directory): @@ -603,11 +609,14 @@ class TrackedObject: thumb_bytes = self.get_thumbnail("webp") - with open(os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb") as f: - f.write(thumb_bytes) + if thumb_bytes: + with open( + os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb" + ) as f: + f.write(thumb_bytes) -def zone_filtered(obj: TrackedObject, object_config): +def zone_filtered(obj: TrackedObject, object_config: dict[str, FilterConfig]) -> bool: object_name = obj.obj_data["label"] if object_name in object_config: @@ -657,9 +666,9 @@ class TrackedObjectAttribute: def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]: """Find the best attribute for each object and return its ID.""" - best_object_area = None - best_object_id = None - best_object_label = None + best_object_area: float | None = None + best_object_id: str | None = None + best_object_label: str | None = None for obj in objects: if not box_inside(obj["box"], self.box): diff --git a/frigate/util/config.py b/frigate/util/config.py index 5b4671b75..56f5662fc 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -363,6 +363,10 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] if days: if mode == "all": continuous["days"] = days + + # if a user was keeping all for number of days + # we need to keep motion and all for that number of days + motion["days"] = days else: motion["days"] = days @@ -380,7 +384,7 @@ 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 ["provider", "base_url", "api_key"]: + if key not in ["enabled", "model", "provider", "base_url", "api_key"]: new_object_config["genai"][key] = global_genai[key] else: new_genai_config[key] = global_genai[key] diff --git a/frigate/util/model.py b/frigate/util/model.py index 65f9b6032..9d042d13f 100644 --- a/frigate/util/model.py +++ b/frigate/util/model.py @@ -284,7 +284,7 @@ def post_process_yolox( def get_ort_providers( - force_cpu: bool = False, device: str = "AUTO", requires_fp16: bool = False + force_cpu: bool = False, device: str | None = "AUTO", requires_fp16: bool = False ) -> tuple[list[str], list[dict[str, Any]]]: if force_cpu: return ( @@ -301,7 +301,7 @@ def get_ort_providers( for provider in ort.get_available_providers(): if provider == "CUDAExecutionProvider": - device_id = 0 if not device.isdigit() else int(device) + device_id = 0 if (not device or not device.isdigit()) else int(device) providers.append(provider) options.append( { diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py new file mode 100644 index 000000000..fb292b84c --- /dev/null +++ b/frigate/util/rknn_converter.py @@ -0,0 +1,470 @@ +"""RKNN model conversion utility for Frigate.""" + +import fcntl +import logging +import os +import subprocess +import sys +import time +from pathlib import Path +from typing import Optional + +logger = logging.getLogger(__name__) + +MODEL_TYPE_CONFIGS = { + "yolo-generic": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "yolonas": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "yolox": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "jina-clip-v1-vision": { + "mean_values": [[0.48145466 * 255, 0.4578275 * 255, 0.40821073 * 255]], + "std_values": [[0.26862954 * 255, 0.26130258 * 255, 0.27577711 * 255]], + "target_platform": None, # Will be set dynamically + }, + "arcface-r100": { + "mean_values": [[127.5, 127.5, 127.5]], + "std_values": [[127.5, 127.5, 127.5]], + "target_platform": None, # Will be set dynamically + }, +} + + +def get_rknn_model_type(model_path: str) -> str | None: + if all(keyword in str(model_path) for keyword in ["jina-clip-v1", "vision"]): + return "jina-clip-v1-vision" + + model_name = os.path.basename(str(model_path)).lower() + + if "arcface" in model_name: + return "arcface-r100" + + if any(keyword in model_name for keyword in ["yolo", "yolox", "yolonas"]): + return model_name + + return None + + +def is_rknn_compatible(model_path: str, model_type: str | None = None) -> bool: + """ + Check if a model is compatible with RKNN conversion. + + Args: + model_path: Path to the model file + model_type: Type of the model (if known) + + Returns: + True if the model is RKNN-compatible, False otherwise + """ + soc = get_soc_type() + if soc is None: + return False + + if not model_type: + model_type = get_rknn_model_type(model_path) + + if model_type and model_type in MODEL_TYPE_CONFIGS: + return True + + return False + + +def ensure_torch_dependencies() -> bool: + """Dynamically install torch dependencies if not available.""" + try: + import torch # type: ignore + + logger.debug("PyTorch is already available") + return True + except ImportError: + logger.info("PyTorch not found, attempting to install...") + + try: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "--break-system-packages", + "torch", + "torchvision", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + import torch # type: ignore # noqa: F401 + + logger.info("PyTorch installed successfully") + return True + except (subprocess.CalledProcessError, ImportError) as e: + logger.error(f"Failed to install PyTorch: {e}") + return False + + +def ensure_rknn_toolkit() -> bool: + """Ensure RKNN toolkit is available.""" + try: + from rknn.api import RKNN # type: ignore # noqa: F401 + + logger.debug("RKNN toolkit is already available") + return True + except ImportError as e: + logger.error(f"RKNN toolkit not found. Please ensure it's installed. {e}") + return False + + +def get_soc_type() -> Optional[str]: + """Get the SoC type from device tree.""" + try: + with open("/proc/device-tree/compatible") as file: + soc = file.read().split(",")[-1].strip("\x00") + return soc + except FileNotFoundError: + logger.debug("Could not determine SoC type from device tree") + return None + + +def convert_onnx_to_rknn( + onnx_path: str, + output_path: str, + model_type: str, + quantization: bool = False, + soc: Optional[str] = None, +) -> bool: + """ + Convert ONNX model to RKNN format. + + Args: + onnx_path: Path to input ONNX model + output_path: Path for output RKNN model + model_type: Type of model (yolo-generic, yolonas, yolox, ssd) + quantization: Whether to use 8-bit quantization (i8) or 16-bit float (fp16) + soc: Target SoC platform (auto-detected if None) + + Returns: + True if conversion successful, False otherwise + """ + if not ensure_torch_dependencies(): + logger.debug("PyTorch dependencies not available") + return False + + if not ensure_rknn_toolkit(): + logger.debug("RKNN toolkit not available") + return False + + # Get SoC type if not provided + if soc is None: + soc = get_soc_type() + if soc is None: + logger.debug("Could not determine SoC type") + return False + + # Get model config for the specified type + if model_type not in MODEL_TYPE_CONFIGS: + logger.debug(f"Unsupported model type: {model_type}") + return False + + config = MODEL_TYPE_CONFIGS[model_type].copy() + config["target_platform"] = soc + + try: + from rknn.api import RKNN # type: ignore + + logger.info(f"Converting {onnx_path} to RKNN format for {soc}") + rknn = RKNN(verbose=True) + rknn.config(**config) + + if model_type == "jina-clip-v1-vision": + load_output = rknn.load_onnx( + model=onnx_path, + inputs=["pixel_values"], + input_size_list=[[1, 3, 224, 224]], + ) + elif model_type == "arcface-r100": + load_output = rknn.load_onnx( + model=onnx_path, + inputs=["data"], + input_size_list=[[1, 3, 112, 112]], + ) + else: + load_output = rknn.load_onnx(model=onnx_path) + + if load_output != 0: + logger.error("Failed to load ONNX model") + return False + + if rknn.build(do_quantization=quantization) != 0: + logger.error("Failed to build RKNN model") + return False + + if rknn.export_rknn(output_path) != 0: + logger.error("Failed to export RKNN model") + return False + + logger.info(f"Successfully converted model to {output_path}") + return True + + except Exception as e: + logger.error(f"Error during RKNN conversion: {e}") + return False + + +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: + """ + Wait for another process to complete the conversion. + + Args: + rknn_path: Path to the expected RKNN model + lock_file_path: Path to the lock file to monitor + timeout: Maximum time to wait in seconds + + Returns: + True if RKNN model appears, False if timeout + """ + start_time = time.time() + while time.time() - start_time < timeout: + # Check if RKNN model appeared + if rknn_path.exists(): + logger.info(f"RKNN model appeared: {rknn_path}") + return True + + # Check if lock file is gone (conversion completed or failed) + if not lock_file_path.exists(): + logger.info("Lock file removed, checking for RKNN model...") + if rknn_path.exists(): + logger.info(f"RKNN model found after lock removal: {rknn_path}") + return True + else: + logger.warning( + "Lock file removed but RKNN model not found, conversion may have failed" + ) + return False + + # Check if lock is stale + if is_lock_stale(lock_file_path): + logger.warning("Lock file is stale, attempting to clean up and retry...") + cleanup_stale_lock(lock_file_path) + # Try to acquire lock again + if acquire_conversion_lock(lock_file_path, timeout=60): + try: + # Check if RKNN file appeared while waiting + if rknn_path.exists(): + logger.info(f"RKNN model appeared while waiting: {rknn_path}") + return True + + # Convert ONNX to RKNN + logger.info( + f"Retrying conversion of {rknn_path} after stale lock cleanup..." + ) + + # Get the original model path from rknn_path + base_path = rknn_path.parent / rknn_path.stem + onnx_path = base_path.with_suffix(".onnx") + + if onnx_path.exists(): + if convert_onnx_to_rknn( + str(onnx_path), str(rknn_path), model_type, False + ): + return True + + logger.error("Failed to convert model after stale lock cleanup") + return False + + finally: + release_conversion_lock(lock_file_path) + + logger.debug("Waiting for RKNN model to appear...") + time.sleep(1) + + logger.warning(f"Timeout waiting for RKNN model: {rknn_path}") + return False + + +def auto_convert_model( + model_path: str, model_type: str | None = None, quantization: bool = False +) -> Optional[str]: + """ + Automatically convert a model to RKNN format if needed. + + Args: + model_path: Path to the model file + model_type: Type of the model + quantization: Whether to use quantization + + Returns: + Path to the RKNN model if successful, None otherwise + """ + if model_path.endswith(".rknn"): + return model_path + + # Check if equivalent .rknn file exists + base_path = Path(model_path) + if base_path.suffix.lower() in [".onnx", ""]: + base_name = base_path.stem if base_path.suffix else base_path.name + rknn_path = base_path.parent / f"{base_name}.rknn" + + if rknn_path.exists(): + logger.info(f"Found existing RKNN model: {rknn_path}") + return str(rknn_path) + + lock_file_path = base_path.parent / f"{base_name}.conversion.lock" + + if acquire_conversion_lock(lock_file_path): + try: + if rknn_path.exists(): + logger.info( + f"RKNN model appeared while waiting for lock: {rknn_path}" + ) + return str(rknn_path) + + logger.info(f"Converting {model_path} to RKNN format...") + rknn_path.parent.mkdir(parents=True, exist_ok=True) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if convert_onnx_to_rknn( + str(base_path), str(rknn_path), model_type, quantization + ): + return str(rknn_path) + else: + logger.error(f"Failed to convert {model_path} to RKNN format") + return None + + finally: + release_conversion_lock(lock_file_path) + else: + logger.info( + f"Another process is converting {model_path}, waiting for completion..." + ) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if wait_for_conversion_completion(model_type, rknn_path, lock_file_path): + return str(rknn_path) + else: + logger.error(f"Timeout waiting for conversion of {model_path}") + return None + + return None diff --git a/frigate/util/services.py b/frigate/util/services.py index 185770eb7..50aa2e2b7 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -6,6 +6,7 @@ import logging import os import re import resource +import shutil import signal import subprocess as sp import traceback @@ -22,6 +23,7 @@ from frigate.const import ( DRIVER_ENV_VAR, FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_VAAPI, + SHM_FRAMES_VAR, ) from frigate.util.builtin import clean_camera_user_pass, escape_special_characters @@ -768,3 +770,65 @@ def set_file_limit() -> None: logger.debug( f"File limit set. New soft limit: {new_soft}, Hard limit remains: {current_hard}" ) + + +def get_fs_type(path: str) -> str: + bestMatch = "" + fsType = "" + for part in psutil.disk_partitions(all=True): + if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint): + fsType = part.fstype + bestMatch = part.mountpoint + return fsType + + +def calculate_shm_requirements(config) -> dict: + try: + storage_stats = shutil.disk_usage("/dev/shm") + except (FileNotFoundError, OSError): + return {} + + total_mb = round(storage_stats.total / pow(2, 20), 1) + used_mb = round(storage_stats.used / pow(2, 20), 1) + free_mb = round(storage_stats.free / pow(2, 20), 1) + + # required for log files + nginx cache + min_req_shm = 40 + 10 + + if config.birdseye.restream: + min_req_shm += 8 + + available_shm = total_mb - min_req_shm + cam_total_frame_size = 0.0 + + for camera in config.cameras.values(): + if camera.enabled_in_config and camera.detect.width and camera.detect.height: + cam_total_frame_size += round( + (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, + 1, + ) + + # leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them. + cam_total_frame_size += 2 * round( + (1280 * 720 * 1.5 + 270480) / 1048576, + 1, + ) + + shm_frame_count = min( + int(os.environ.get(SHM_FRAMES_VAR, "50")), + int(available_shm / cam_total_frame_size), + ) + + # minimum required shm recommendation + min_shm = round(min_req_shm + cam_total_frame_size * 20) + + return { + "total": total_mb, + "used": used_mb, + "free": free_mb, + "mount_type": get_fs_type("/dev/shm"), + "available": round(available_shm, 1), + "camera_frame_size": cam_total_frame_size, + "shm_frame_count": shm_frame_count, + "min_shm": min_shm, + } diff --git a/frigate/video.py b/frigate/video.py index dc3f1c430..57b620e3a 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -71,7 +71,7 @@ def stop_ffmpeg(ffmpeg_process: sp.Popen[Any], logger: logging.Logger): def start_or_restart_ffmpeg( ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None -): +) -> sp.Popen[Any]: if ffmpeg_process is not None: stop_ffmpeg(ffmpeg_process, logger) @@ -96,7 +96,7 @@ def start_or_restart_ffmpeg( def capture_frames( - ffmpeg_process, + ffmpeg_process: sp.Popen[Any], config: CameraConfig, shm_frame_count: int, frame_index: int, @@ -107,7 +107,7 @@ def capture_frames( skipped_fps: Value, current_frame: Value, stop_event: MpEvent, -): +) -> None: frame_size = frame_shape[0] * frame_shape[1] frame_rate = EventsPerSecond() frame_rate.start() @@ -196,6 +196,7 @@ class CameraWatchdog(threading.Thread): self.config_subscriber = CameraConfigUpdateSubscriber( None, {config.name: config}, [CameraConfigUpdateEnum.enabled] ) + self.requestor = InterProcessRequestor() self.was_enabled = self.config.enabled def _update_enabled_state(self) -> bool: @@ -245,6 +246,14 @@ class CameraWatchdog(threading.Thread): else: self.logger.debug(f"Disabling camera {self.config.name}") self.stop_all_ffmpeg() + + # update camera status + self.requestor.send_data( + f"{self.config.name}/status/detect", "disabled" + ) + self.requestor.send_data( + f"{self.config.name}/status/record", "disabled" + ) self.was_enabled = enabled continue @@ -254,6 +263,7 @@ class CameraWatchdog(threading.Thread): now = datetime.datetime.now().timestamp() if not self.capture_thread.is_alive(): + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") self.camera_fps.value = 0 self.logger.error( f"Ffmpeg process crashed unexpectedly for {self.config.name}." @@ -263,6 +273,9 @@ class CameraWatchdog(threading.Thread): self.fps_overflow_count += 1 if self.fps_overflow_count == 3: + self.requestor.send_data( + f"{self.config.name}/status/detect", "offline" + ) self.fps_overflow_count = 0 self.camera_fps.value = 0 self.logger.info( @@ -270,6 +283,7 @@ class CameraWatchdog(threading.Thread): ) self.reset_capture_thread(drain_output=False) elif now - self.capture_thread.current_frame.value > 20: + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") self.camera_fps.value = 0 self.logger.info( f"No frames received from {self.config.name} in 20 seconds. Exiting ffmpeg..." @@ -277,6 +291,7 @@ class CameraWatchdog(threading.Thread): self.reset_capture_thread() else: # process is running normally + self.requestor.send_data(f"{self.config.name}/status/detect", "online") self.fps_overflow_count = 0 for p in self.ffmpeg_other_processes: @@ -302,13 +317,27 @@ class CameraWatchdog(threading.Thread): p["logpipe"], ffmpeg_process=p["process"], ) + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role}", "offline" + ) + continue else: + self.requestor.send_data( + f"{self.config.name}/status/record", "online" + ) p["latest_segment_time"] = latest_segment_time if poll is None: continue + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role}", "offline" + ) + p["logpipe"].dump() p["process"] = start_or_restart_ffmpeg( p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"] diff --git a/notebooks/YOLO_NAS_Pretrained_Export.ipynb b/notebooks/YOLO_NAS_Pretrained_Export.ipynb index 4e0439e9e..e9ee22314 100644 --- a/notebooks/YOLO_NAS_Pretrained_Export.ipynb +++ b/notebooks/YOLO_NAS_Pretrained_Export.ipynb @@ -19,8 +19,8 @@ }, "outputs": [], "source": [ - "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.11/dist-packages/super_gradients/training/pretrained_models.py\n", - "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.11/dist-packages/super_gradients/training/utils/checkpoint_utils.py" + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/pretrained_models.py\n", + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/utils/checkpoint_utils.py" ] }, { diff --git a/web/public/locales/bg/common.json b/web/public/locales/bg/common.json index 8c5519885..e69813950 100644 --- a/web/public/locales/bg/common.json +++ b/web/public/locales/bg/common.json @@ -106,5 +106,7 @@ }, "label": { "back": "Върни се" - } + }, + "selectItem": "Избери {{item}}", + "readTheDocumentation": "Прочетете документацията" } diff --git a/web/public/locales/ca/common.json b/web/public/locales/ca/common.json index c981fd716..7c11a8139 100644 --- a/web/public/locales/ca/common.json +++ b/web/public/locales/ca/common.json @@ -261,5 +261,6 @@ "title": "404", "desc": "Pàgina no trobada" }, - "selectItem": "Selecciona {{item}}" + "selectItem": "Selecciona {{item}}", + "readTheDocumentation": "Llegir la documentació" } diff --git a/web/public/locales/ca/components/dialog.json b/web/public/locales/ca/components/dialog.json index b2759e896..20988372a 100644 --- a/web/public/locales/ca/components/dialog.json +++ b/web/public/locales/ca/components/dialog.json @@ -110,5 +110,12 @@ "error": "No s'ha pogut suprimir: {{error}}" } } + }, + "imagePicker": { + "selectImage": "Selecciona la miniatura d'un objecte rastrejat", + "search": { + "placeholder": "Cerca per etiqueta o subetiqueta..." + }, + "noImages": "No s'han trobat miniatures per a aquesta càmera" } } diff --git a/web/public/locales/ca/components/filter.json b/web/public/locales/ca/components/filter.json index aa02310f7..3d521fb96 100644 --- a/web/public/locales/ca/components/filter.json +++ b/web/public/locales/ca/components/filter.json @@ -122,5 +122,13 @@ }, "motion": { "showMotionOnly": "Mostar només el moviment" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Totes les classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" } } diff --git a/web/public/locales/ca/views/configEditor.json b/web/public/locales/ca/views/configEditor.json index 8d47ea04c..bd3149a3f 100644 --- a/web/public/locales/ca/views/configEditor.json +++ b/web/public/locales/ca/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Error al desar la configuració" } }, - "confirm": "Sortir sense desar?" + "confirm": "Sortir sense desar?", + "safeConfigEditor": "Editor de Configuració (Mode Segur)", + "safeModeDescription": "Frigate està en mode segur a causa d'un error de validació de la configuració." } diff --git a/web/public/locales/ca/views/explore.json b/web/public/locales/ca/views/explore.json index 07f787ed3..933082609 100644 --- a/web/public/locales/ca/views/explore.json +++ b/web/public/locales/ca/views/explore.json @@ -97,7 +97,8 @@ "success": { "updatedSublabel": "Subetiqueta actualitzada amb èxit.", "updatedLPR": "Matrícula actualitzada amb èxit.", - "regenerate": "El {{provider}} ha sol·licitat una nova descripció. En funció de la velocitat del vostre proveïdor, la nova descripció pot trigar un temps a regenerar-se." + "regenerate": "El {{provider}} ha sol·licitat una nova descripció. En funció de la velocitat del vostre proveïdor, la nova descripció pot trigar un temps a regenerar-se.", + "audioTranscription": "Transcripció d'àudio sol·licitada amb èxit." }, "error": { "regenerate": "No s'ha pogut contactar amb {{provider}} per obtenir una nova descripció: {{errorMessage}}", diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json index dd091b7de..eded31f4c 100644 --- a/web/public/locales/ca/views/live.json +++ b/web/public/locales/ca/views/live.json @@ -32,7 +32,15 @@ "label": "Fer clic a la imatge per centrar la càmera PTZ" } }, - "presets": "Predefinits de la càmera PTZ" + "presets": "Predefinits de la càmera PTZ", + "focus": { + "in": { + "label": "Enfoca la càmera PTZ aprop" + }, + "out": { + "label": "Enfoca la càmera PTZ lluny" + } + } }, "documentTitle": "Directe - Frigate", "documentTitle.withCamera": "{{camera}} - Directe - Frigate", @@ -154,5 +162,9 @@ "label": "Editar grup de càmeres" }, "exitEdit": "Sortir de l'edició" + }, + "transcription": { + "enable": "Habilita la transcripció d'àudio en temps real", + "disable": "Deshabilita la transcripció d'àudio en temps real" } } diff --git a/web/public/locales/cs/common.json b/web/public/locales/cs/common.json index 08cff4992..4b728603d 100644 --- a/web/public/locales/cs/common.json +++ b/web/public/locales/cs/common.json @@ -261,5 +261,7 @@ "admin": "Správce", "viewer": "Divák", "desc": "Správci mají plný přístup ke všem funkcím v uživatelském rozhraní Frigate. Diváci jsou omezeni na sledování kamer, položek přehledu a historických záznamů v UI." - } + }, + "selectItem": "Vybrat {{item}}", + "readTheDocumentation": "Přečtěte si dokumentaci" } diff --git a/web/public/locales/da/common.json b/web/public/locales/da/common.json index b0bbd3d5f..7625631c2 100644 --- a/web/public/locales/da/common.json +++ b/web/public/locales/da/common.json @@ -254,5 +254,6 @@ "title": "404", "desc": "Side ikke fundet" }, - "selectItem": "Vælg {{item}}" + "selectItem": "Vælg {{item}}", + "readTheDocumentation": "Læs dokumentationen" } diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json index 8a3eff88c..5eff84221 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -38,7 +38,7 @@ "1hour": "1 Stunde", "lastWeek": "Letzte Woche", "h": "{{time}} Stunde", - "ago": "{{timeAgo}} her", + "ago": "vor {{timeAgo}}", "untilRestart": "Bis zum Neustart", "justNow": "Gerade", "pm": "nachmittags", @@ -160,7 +160,15 @@ "sk": "Slowakisch", "yue": "粵語 (Kantonesisch)", "th": "ไทย (Thailändisch)", - "ca": "Català (Katalanisch)" + "ca": "Català (Katalanisch)", + "ur": "اردو (Urdu)", + "ptBR": "Portugiesisch (Brasilianisch)", + "sr": "Српски (Serbisch)", + "sl": "Slovenščina (Slowenisch)", + "lt": "Lietuvių (Litauisch)", + "bg": "Български (bulgarisch)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)" }, "appearance": "Erscheinung", "theme": { @@ -168,7 +176,7 @@ "blue": "Blau", "green": "Grün", "default": "Standard", - "nord": "Norden", + "nord": "Nord", "red": "Rot", "contrast": "Hoher Kontrast", "highcontrast": "Hoher Kontrast" @@ -260,6 +268,7 @@ "documentTitle": "Nicht gefunden - Frigate" }, "selectItem": "Wähle {{item}}", + "readTheDocumentation": "Dokumentation lesen", "accessDenied": { "desc": "Du hast keine Berechtigung diese Seite anzuzeigen.", "documentTitle": "Zugang verweigert - Frigate", diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json index cedd1c114..578f02773 100644 --- a/web/public/locales/de/components/dialog.json +++ b/web/public/locales/de/components/dialog.json @@ -119,5 +119,12 @@ "markAsReviewed": "Als geprüft markieren", "deleteNow": "Jetzt löschen" } + }, + "imagePicker": { + "selectImage": "Vorschaubild eines verfolgten Objekts selektieren", + "search": { + "placeholder": "Nach Label oder Unterlabel suchen..." + }, + "noImages": "Kein Vorschaubild für diese Kamera gefunden" } } diff --git a/web/public/locales/de/components/filter.json b/web/public/locales/de/components/filter.json index a2c7db779..52934ea9c 100644 --- a/web/public/locales/de/components/filter.json +++ b/web/public/locales/de/components/filter.json @@ -122,5 +122,13 @@ "loading": "Lade bekannte Nummernschilder…", "placeholder": "Tippe, um Kennzeichen zu suchen…", "selectPlatesFromList": "Wählen eine oder mehrere Kennzeichen aus der Liste aus." + }, + "classes": { + "label": "Klassen", + "all": { + "title": "Alle Klassen" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klassen" } } diff --git a/web/public/locales/de/views/configEditor.json b/web/public/locales/de/views/configEditor.json index 7f975e31b..86959e126 100644 --- a/web/public/locales/de/views/configEditor.json +++ b/web/public/locales/de/views/configEditor.json @@ -12,5 +12,7 @@ } }, "documentTitle": "Konfigurationseditor – Frigate", - "confirm": "Verlassen ohne zu Speichern?" + "confirm": "Verlassen ohne zu Speichern?", + "safeConfigEditor": "Konfiguration Editor (abgesicherter Modus)", + "safeModeDescription": "Frigate ist aufgrund eines Konfigurationsvalidierungsfehlers im abgesicherten Modus." } diff --git a/web/public/locales/de/views/events.json b/web/public/locales/de/views/events.json index 2a38ac029..e9bdcf4ff 100644 --- a/web/public/locales/de/views/events.json +++ b/web/public/locales/de/views/events.json @@ -34,5 +34,7 @@ "markAsReviewed": "Als geprüft kennzeichnen", "selected_one": "{{count}} ausgewählt", "selected_other": "{{count}} ausgewählt", - "detected": "erkannt" + "detected": "erkannt", + "suspiciousActivity": "Verdächtige Aktivität", + "threateningActivity": "Bedrohliche Aktivität" } diff --git a/web/public/locales/de/views/explore.json b/web/public/locales/de/views/explore.json index ee518fc11..04409fd84 100644 --- a/web/public/locales/de/views/explore.json +++ b/web/public/locales/de/views/explore.json @@ -17,12 +17,14 @@ "success": { "updatedSublabel": "Unterkategorie erfolgreich aktualisiert.", "updatedLPR": "Nummernschild erfolgreich aktualisiert.", - "regenerate": "Eine neue Beschreibung wurde von {{provider}} angefordert. Je nach Geschwindigkeit des Anbieters kann es einige Zeit dauern, bis die neue Beschreibung generiert ist." + "regenerate": "Eine neue Beschreibung wurde von {{provider}} angefordert. Je nach Geschwindigkeit des Anbieters kann es einige Zeit dauern, bis die neue Beschreibung generiert ist.", + "audioTranscription": "Audio Transkription erfolgreich angefordert." }, "error": { "regenerate": "Der Aufruf von {{provider}} für eine neue Beschreibung ist fehlgeschlagen: {{errorMessage}}", "updatedSublabelFailed": "Untekategorie konnte nicht aktualisiert werden: {{errorMessage}}", - "updatedLPRFailed": "Aktualisierung des Kennzeichens fehlgeschlagen: {{errorMessage}}" + "updatedLPRFailed": "Aktualisierung des Kennzeichens fehlgeschlagen: {{errorMessage}}", + "audioTranscription": "Die Anforderung der Audio Transkription ist fehlgeschlagen: {{errorMessage}}" } } }, @@ -67,6 +69,9 @@ }, "snapshotScore": { "label": "Schnappschuss Bewertung" + }, + "score": { + "label": "Ergebnis" } }, "documentTitle": "Erkunde - Frigate", @@ -182,6 +187,14 @@ }, "deleteTrackedObject": { "label": "Dieses verfolgte Objekt löschen" + }, + "audioTranscription": { + "aria": "Audio Transkription anfordern", + "label": "Transkribieren" + }, + "addTrigger": { + "aria": "Einen Trigger für dieses verfolgte Objekt hinzufügen", + "label": "Trigger hinzufügen" } }, "dialog": { @@ -203,5 +216,11 @@ "fetchingTrackedObjectsFailed": "Fehler beim Abrufen von verfolgten Objekten: {{errorMessage}}", "trackedObjectsCount_one": "{{count}} verfolgtes Objekt ", "trackedObjectsCount_other": "{{count}} verfolgte Objekte ", - "exploreMore": "Erkunde mehr {{label}} Objekte" + "exploreMore": "Erkunde mehr {{label}} Objekte", + "aiAnalysis": { + "title": "KI-Analyse" + }, + "concerns": { + "label": "Bedenken" + } } diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json index 960c555db..cda458b65 100644 --- a/web/public/locales/de/views/faceLibrary.json +++ b/web/public/locales/de/views/faceLibrary.json @@ -46,7 +46,7 @@ "train": { "title": "Trainiere", "aria": "Wähle Training", - "empty": "Es gibt keine aktuellen Versuche zurGesichtserkennung" + "empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung" }, "deleteFaceLibrary": { "title": "Lösche Name", diff --git a/web/public/locales/de/views/live.json b/web/public/locales/de/views/live.json index 318c2b720..fea1cabd8 100644 --- a/web/public/locales/de/views/live.json +++ b/web/public/locales/de/views/live.json @@ -41,6 +41,14 @@ "center": { "label": "Klicken Sie in den Rahmen, um die PTZ-Kamera zu zentrieren" } + }, + "focus": { + "in": { + "label": "PTZ Kamera hinein fokussieren" + }, + "out": { + "label": "PTZ Kamera hinaus fokussieren" + } } }, "documentTitle": "Live - Frigate", @@ -100,7 +108,7 @@ "tips": "Ihr Gerät muss die Funktion unterstützen und WebRTC muss für die bidirektionale Kommunikation konfiguriert sein.", "tips.documentation": "Dokumentation lesen ", "available": "Für diesen Stream ist eine Zwei-Wege-Sprechfunktion verfügbar", - "unavailable": "Für diesen Stream ist keine Zwei-Wege-Kommunikation möglich." + "unavailable": "Zwei-Wege-Kommunikation für diesen Stream nicht verfügbar" }, "lowBandwidth": { "tips": "Die Live-Ansicht befindet sich aufgrund von Puffer- oder Stream-Fehlern im Modus mit geringer Bandbreite.", @@ -146,7 +154,8 @@ "cameraEnabled": "Kamera aktiviert", "autotracking": "Autotracking", "audioDetection": "Audioerkennung", - "title": "{{camera}} Einstellungen" + "title": "{{camera}} Einstellungen", + "transcription": "Audio Transkription" }, "history": { "label": "Historisches Filmmaterial zeigen" @@ -154,5 +163,9 @@ "audio": "Audio", "suspend": { "forTime": "Aussetzen für: " + }, + "transcription": { + "enable": "Live Audio Transkription einschalten", + "disable": "Live Audio Transkription ausschalten" } } diff --git a/web/public/locales/de/views/search.json b/web/public/locales/de/views/search.json index c3800ab28..5729716d8 100644 --- a/web/public/locales/de/views/search.json +++ b/web/public/locales/de/views/search.json @@ -58,7 +58,7 @@ "title": "Wie man Textfilter verwendet" }, "searchType": { - "thumbnail": "Miniaturansicht", + "thumbnail": "Vorschaubild", "description": "Beschreibung" } }, diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json index 29c5d6ece..f01716905 100644 --- a/web/public/locales/de/views/settings.json +++ b/web/public/locales/de/views/settings.json @@ -8,7 +8,7 @@ "general": "Allgemeine Einstellungen – Frigate", "frigatePlus": "Frigate+ Einstellungen – Frigate", "classification": "Klassifizierungseinstellungen – Frigate", - "motionTuner": "Bewegungstuner – Frigate", + "motionTuner": "Bewegungserkennungs-Optimierer – Frigate", "notifications": "Benachrichtigungs-Einstellungen", "enrichments": "Erweiterte Statistiken - Frigate" }, @@ -17,12 +17,12 @@ "cameras": "Kameraeinstellungen", "classification": "Klassifizierung", "masksAndZones": "Maskierungen / Zonen", - "motionTuner": "Bewegungstuner", + "motionTuner": "Bewegungserkennungs-Optimierer", "debug": "Debug", "frigateplus": "Frigate+", "users": "Benutzer", "notifications": "Benachrichtigungen", - "enrichments": "Verbesserungen" + "enrichments": "Erkennungsfunktionen" }, "dialog": { "unsavedChanges": { @@ -178,7 +178,44 @@ "detections": "Erkennungen ", "desc": "Aktiviere/deaktiviere Benachrichtigungen und Erkennungen für diese Kamera vorübergehend, bis Frigate neu gestartet wird. Wenn deaktiviert, werden keine neuen Überprüfungseinträge erstellt. " }, - "title": "Kamera-Einstellungen" + "title": "Kameraeinstellungen", + "object_descriptions": { + "title": "Generative KI-Objektbeschreibungen", + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für verfolgte Objekte auf dieser Kamera angefordert." + }, + "cameraConfig": { + "ffmpeg": { + "roles": "Rollen", + "pathRequired": "Stream-Pfad ist erforderlich", + "path": "Stream-Pfad", + "inputs": "Eingabe Streams", + "pathPlaceholder": "rtsp://...", + "rolesRequired": "Mindestens eine Rolle ist erforderlich", + "rolesUnique": "Jede Rolle (Audio, Erkennung, Aufzeichnung) kann nur einem Stream zugewiesen werden", + "addInput": "Eingabe-Stream hinzufügen", + "removeInput": "Eingabe-Stream entfernen", + "inputsRequired": "Mindestens ein Eingabe-Stream ist erforderlich" + }, + "enabled": "Aktiviert", + "namePlaceholder": "z. B., Vorder_Türe", + "nameInvalid": "Der Name der Kamera darf nur Buchstaben, Zahlen, Unterstriche oder Bindestriche enthalten", + "name": "Kamera Name", + "edit": "Kamera bearbeiten", + "add": "Kamera hinzufügen", + "description": "Kameraeinstellungen einschließlich Stream-Eingänge und Rollen konfigurieren.", + "nameRequired": "Kameraname ist erforderlich", + "toast": { + "success": "Kamera {{cameraName}} erfolgreich gespeichert" + } + }, + "backToSettings": "Zurück zu den Kamera Einstellungen", + "selectCamera": "Kamera wählen", + "editCamera": "Kamera bearbeiten:", + "addCamera": "Neue Kamera hinzufügen", + "review_descriptions": { + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für Überprüfungselemente auf dieser Kamera angefordert.", + "title": "Beschreibungen zur generativen KI-Überprüfung" + } }, "masksAndZones": { "form": { @@ -397,7 +434,12 @@ "desc": "Einen Rahmen für den an den Objektdetektor übermittelten Interessensbereich anzeigen" }, "title": "Debug", - "desc": "Die Debug-Ansicht zeigt eine Echtzeitansicht der verfolgten Objekte und ihrer Statistiken. Die Objektliste zeigt eine zeitverzögerte Zusammenfassung der erkannten Objekte." + "desc": "Die Debug-Ansicht zeigt eine Echtzeitansicht der verfolgten Objekte und ihrer Statistiken. Die Objektliste zeigt eine zeitverzögerte Zusammenfassung der erkannten Objekte.", + "paths": { + "title": "Pfade", + "desc": "Wichtige Punkte des Pfads des verfolgten Objekts anzeigen", + "tips": "

Pfade


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

" + } }, "motionDetectionTuner": { "Threshold": { @@ -420,7 +462,7 @@ "desc": "Der Wert für die Konturfläche wird verwendet, um zu bestimmen, welche Gruppen von veränderten Pixeln als Bewegung gelten. Standard: 10" }, "title": "Bewegungserkennungs-Optimierer", - "unsavedChanges": "Nicht gespeicherte Änderungen am Bewegungstuner ({{camera}})" + "unsavedChanges": "Nicht gespeicherte Änderungen im Bewegungserkennungs-Optimierer ({{camera}})" }, "users": { "addUser": "Benutzer hinzufügen", @@ -679,5 +721,100 @@ "success": "Die Einstellungen für die Verbesserungen wurden gespeichert. Starten Sie Frigate neu, um Ihre Änderungen zu übernehmen.", "error": "Konfigurationsänderungen konnten nicht gespeichert werden: {{errorMessage}}" } + }, + "triggers": { + "documentTitle": "Auslöser", + "management": { + "title": "Auslöser Verwaltung", + "desc": "Auslöser für {{camera}} verwalten. Verwenden Sie den Vorschaubild Typ, um ähnliche Vorschaubilder wie das ausgewählte verfolgte Objekt auszulösen, und den Beschreibungstyp, um ähnliche Beschreibungen wie den von Ihnen angegebenen Text auszulösen." + }, + "addTrigger": "Auslöser hinzufügen", + "table": { + "name": "Name", + "type": "Typ", + "content": "Inhalt", + "threshold": "Schwellenwert", + "actions": "Aktionen", + "noTriggers": "Für diese Kamera sind keine Auslöser konfiguriert.", + "edit": "Bearbeiten", + "deleteTrigger": "Auslöser löschen", + "lastTriggered": "Zuletzt ausgelöst" + }, + "type": { + "thumbnail": "Vorschaubild", + "description": "Beschreibung" + }, + "actions": { + "alert": "Als Alarm markieren", + "notification": "Benachrichtigung senden" + }, + "dialog": { + "createTrigger": { + "title": "Auslöser erstellen", + "desc": "Auslöser für Kamera {{camera}} erstellen" + }, + "editTrigger": { + "title": "Auslöser bearbeiten", + "desc": "Einstellungen für Kamera {{camera}} bearbeiten" + }, + "deleteTrigger": { + "title": "Auslöser löschen", + "desc": "Sind Sie sicher, dass Sie den Auslöser {{triggerName}} löschen wollen? Dies kann nicht Rückgängig gemacht werden." + }, + "form": { + "name": { + "title": "Name", + "placeholder": "Auslöser Name eingeben", + "error": { + "minLength": "Der Name muss mindestens 2 Zeichen lang sein.", + "invalidCharacters": "Der Name darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.", + "alreadyExists": "Ein Auslöser mit diesem Namen existiert bereits für diese Kamera." + } + }, + "enabled": { + "description": "Diesen Auslöser aktivieren oder deaktivieren" + }, + "type": { + "title": "Typ", + "placeholder": "Auslöser Typ wählen" + }, + "content": { + "title": "Inhalt", + "imagePlaceholder": "Ein Bild auswählen", + "textPlaceholder": "Inhaltstext eingeben", + "imageDesc": "Ein Bild auswählen, um diese Aktion auszulösen, wenn ein ähnliches Bild erkannt wird.", + "textDesc": "Einen Text eingeben, um diese Aktion auszulösen, wenn eine ähnliche Beschreibung eines verfolgten Objekts erkannt wird.", + "error": { + "required": "Inhalt ist erforderlich." + } + }, + "threshold": { + "title": "Schwellenwert", + "error": { + "min": "Schwellenwert muss mindestens 0 sein", + "max": "Schwellenwert darf höchstens 1 sein" + } + }, + "actions": { + "title": "Aktionen", + "desc": "Standardmäßig sendet Frigate eine MQTT-Nachricht für alle Trigger. Wähle eine zusätzliche Aktion aus, die ausgeführt werden soll, wenn dieser Trigger ausgelöst wird.", + "error": { + "min": "Mindesten eine Aktion muss ausgewählt sein." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Auslöser {{name}} erfolgreich erstellt.", + "updateTrigger": "Auslöser {{name}} erfolgreich aktualisiert.", + "deleteTrigger": "Auslöser {{name}} erfolgreich gelöscht." + }, + "error": { + "createTriggerFailed": "Auslöser konnte nicht erstellt werden: {{errorMessage}}", + "updateTriggerFailed": "Auslöser könnte nicht aktualisiert werden: {{errorMessage}}", + "deleteTriggerFailed": "Auslöser konnte nicht gelöscht werden: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/el/common.json b/web/public/locales/el/common.json index d521af9a0..4793c77ee 100644 --- a/web/public/locales/el/common.json +++ b/web/public/locales/el/common.json @@ -3,6 +3,7 @@ "untilForTime": "Ως{{time}}", "untilForRestart": "Μέχρι να γίνει επανεκίννηση του Frigate.", "untilRestart": "Μέχρι να γίνει επανεκκίνηση", - "justNow": "Μόλις τώρα" + "justNow": "Μόλις τώρα", + "ago": "{{timeAgo}} Πριν" } } diff --git a/web/public/locales/el/components/camera.json b/web/public/locales/el/components/camera.json index 8d0571fbe..3b364aebc 100644 --- a/web/public/locales/el/components/camera.json +++ b/web/public/locales/el/components/camera.json @@ -1,6 +1,10 @@ { "group": { "add": "Προσθήκη ομάδας καμερών", - "label": "Ομάδες καμερών" + "label": "Ομάδες καμερών", + "edit": "Επεξεργασία ομάδας καμερών", + "delete": { + "label": "Διαγραφή ομάδας κάμερας" + } } } diff --git a/web/public/locales/el/views/explore.json b/web/public/locales/el/views/explore.json index a48e770ea..63a348b3f 100644 --- a/web/public/locales/el/views/explore.json +++ b/web/public/locales/el/views/explore.json @@ -1,3 +1,8 @@ { - "documentTitle": "Εξερευνήστε - Frigate" + "documentTitle": "Εξερευνήστε - Frigate", + "generativeAI": "Παραγωγική τεχνητή νοημοσύνη", + "exploreMore": "Εξερευνήστε περισσότερα αντικείμενα {{label}}", + "exploreIsUnavailable": { + "title": "Η εξερεύνηση δεν είναι διαθέσιμη" + } } diff --git a/web/public/locales/el/views/exports.json b/web/public/locales/el/views/exports.json index e8517ae5c..564e76960 100644 --- a/web/public/locales/el/views/exports.json +++ b/web/public/locales/el/views/exports.json @@ -1,5 +1,6 @@ { "documentTitle": "Εξαγωγή - Frigate", "search": "Αναζήτηση", - "deleteExport": "Διαγραφή εξαγωγής" + "deleteExport": "Διαγραφή εξαγωγής", + "noExports": "Δεν βρέθηκαν εξαγωγές" } diff --git a/web/public/locales/el/views/live.json b/web/public/locales/el/views/live.json index daeb09636..d0f1d7efb 100644 --- a/web/public/locales/el/views/live.json +++ b/web/public/locales/el/views/live.json @@ -2,5 +2,6 @@ "documentTitle": "Ζωντανά - Frigate", "twoWayTalk": { "enable": "Ενεργοποίηση αμφίδρομης επικοινωνίας" - } + }, + "documentTitle.withCamera": "{{camera}} - Live - Frigate" } diff --git a/web/public/locales/el/views/search.json b/web/public/locales/el/views/search.json index 96ca56e0d..2aa91b145 100644 --- a/web/public/locales/el/views/search.json +++ b/web/public/locales/el/views/search.json @@ -3,5 +3,6 @@ "savedSearches": "Αποθηκευμένες Αναζητήσεις", "button": { "clear": "Εκαθάρηση αναζήτησης" - } + }, + "searchFor": "Αναζήτηση {{inputValue}}" } diff --git a/web/public/locales/el/views/system.json b/web/public/locales/el/views/system.json index 3076645d6..e98f9e361 100644 --- a/web/public/locales/el/views/system.json +++ b/web/public/locales/el/views/system.json @@ -1,5 +1,7 @@ { "documentTitle": { - "cameras": "Στατιστικά Καμερών - Frigate" + "cameras": "Στατιστικά Καμερών - Frigate", + "storage": "Στατιστικά αποθήκευσης - Frigate", + "general": "Γενικά στατιστικά στοιχεία - Frigate" } } diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 86304fff3..c22a9227d 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -262,5 +262,6 @@ "title": "404", "desc": "Page not found" }, - "selectItem": "Select {{item}}" + "selectItem": "Select {{item}}", + "readTheDocumentation": "Read the documentation" } diff --git a/web/public/locales/en/components/camera.json b/web/public/locales/en/components/camera.json index e13039b44..864efa6c4 100644 --- a/web/public/locales/en/components/camera.json +++ b/web/public/locales/en/components/camera.json @@ -36,8 +36,7 @@ "audioIsUnavailable": "Audio is unavailable for this stream", "audio": { "tips": { - "title": "Audio must be output from your camera and configured in go2rtc for this stream.", - "document": "Read the documentation " + "title": "Audio must be output from your camera and configured in go2rtc for this stream." } }, "stream": "Stream", diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json index 02ab43c4c..a64dbf594 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -69,8 +69,7 @@ "restreaming": { "disabled": "Restreaming is not enabled for this camera.", "desc": { - "title": "Set up go2rtc for additional live view options and audio for this camera.", - "readTheDocumentation": "Read the documentation" + "title": "Set up go2rtc for additional live view options and audio for this camera." } }, "showStats": { diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json index 98bc7c422..77c626adf 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -34,5 +34,7 @@ "selected_one": "{{count}} selected", "selected_other": "{{count}} selected", "camera": "Camera", - "detected": "detected" + "detected": "detected", + "suspiciousActivity": "Suspicious Activity", + "threateningActivity": "Threatening Activity" } diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index d754fee77..f35cfdc1d 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -24,8 +24,7 @@ "textTokenizer": "Text tokenizer" }, "tips": { - "context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded.", - "documentation": "Read the documentation" + "context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded." }, "error": "An error has occurred. Check Frigate logs." } @@ -73,7 +72,6 @@ "offset": { "label": "Annotation Offset", "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. However, the annotation_offset field can be used to adjust this.", - "documentation": "Read the documentation ", "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.", "toast": { @@ -132,6 +130,9 @@ "label": "Top Score", "info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail." }, + "score": { + "label": "Score" + }, "recognizedLicensePlate": "Recognized License Plate", "estimatedSpeed": "Estimated Speed", "objects": "Objects", @@ -213,5 +214,11 @@ "error": "Failed to delete tracked object: {{errorMessage}}" } } + }, + "aiAnalysis": { + "title": "AI Analysis" + }, + "concerns": { + "label": "Concerns" } } diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index e734ca974..7bd60f95a 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -71,7 +71,6 @@ }, "nofaces": "No faces available", "pixels": "{{area}}px", - "readTheDocs": "Read the documentation", "trainFaceAs": "Train Face as:", "trainFace": "Train Face", "toast": { diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index 2af399296..f20fc5118 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -119,15 +119,13 @@ "title": "Stream", "audio": { "tips": { - "title": "Audio must be output from your camera and configured in go2rtc for this stream.", - "documentation": "Read the documentation " + "title": "Audio must be output from your camera and configured in go2rtc for this stream." }, "available": "Audio is available for this stream", "unavailable": "Audio is not available for this stream" }, "twoWayTalk": { "tips": "Your device must support the feature and WebRTC must be configured for two-way talk.", - "tips.documentation": "Read the documentation ", "available": "Two-way talk is available for this stream", "unavailable": "Two-way talk is unavailable for this stream" }, diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 676430a5f..e7c06b133 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -93,7 +93,6 @@ "semanticSearch": { "title": "Semantic Search", "desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.", - "readTheDocumentation": "Read the Documentation", "reindexNow": { "label": "Reindex Now", "desc": "Reindexing will regenerate embeddings for all tracked object. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.", @@ -120,7 +119,6 @@ "faceRecognition": { "title": "Face Recognition", "desc": "Face recognition allows people to be assigned names and when their face is recognized Frigate will assign the person's name as a sub label. This information is included in the UI, filters, as well as in notifications.", - "readTheDocumentation": "Read the Documentation", "modelSize": { "label": "Model Size", "desc": "The size of the model used for face recognition.", @@ -136,8 +134,7 @@ }, "licensePlateRecognition": { "title": "License Plate Recognition", - "desc": "Frigate can recognize license plates on vehicles and automatically add the detected characters to the recognized_license_plate field or a known name as a sub_label to objects that are of type car. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.", - "readTheDocumentation": "Read the Documentation" + "desc": "Frigate can recognize license plates on vehicles and automatically add the detected characters to the recognized_license_plate field or a known name as a sub_label to objects that are of type car. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street." }, "restart_required": "Restart required (Enrichments settings changed)", "toast": { @@ -168,7 +165,7 @@ "reviewClassification": { "title": "Review Classification", "desc": "Frigate categorizes review items as Alerts and Detections. By default, all person and car objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.", - "readTheDocumentation": "Read the Documentation", + "noDefinedZones": "No zones are defined for this camera.", "objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.", "zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.", @@ -314,7 +311,6 @@ "speedEstimation": { "title": "Speed Estimation", "desc": "Enable speed estimation for objects in this zone. The zone must have exactly 4 points.", - "docs": "Read the documentation", "lineADistance": "Line A distance ({{unit}})", "lineBDistance": "Line B distance ({{unit}})", "lineCDistance": "Line C distance ({{unit}})", @@ -344,16 +340,14 @@ "add": "New Motion Mask", "edit": "Edit Motion Mask", "context": { - "title": "Motion masks are used to prevent unwanted types of motion from triggering detection (example: tree branches, camera timestamps). Motion masks should be used very sparingly, over-masking will make it more difficult for objects to be tracked.", - "documentation": "Read the documentation" + "title": "Motion masks are used to prevent unwanted types of motion from triggering detection (example: tree branches, camera timestamps). Motion masks should be used very sparingly, over-masking will make it more difficult for objects to be tracked." }, "point_one": "{{count}} point", "point_other": "{{count}} points", "clickDrawPolygon": "Click to draw a polygon on the image.", "polygonAreaTooLarge": { "title": "The motion mask is covering {{polygonArea}}% of the camera frame. Large motion masks are not recommended.", - "tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead.", - "documentation": "Read the documentation" + "tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead." }, "toast": { "success": { @@ -419,6 +413,13 @@ "debugging": "Debugging", "objectList": "Object List", "noObjects": "No objects", + "audio": { + "title": "Audio", + "noAudioDetections": "No audio detections", + "score": "score", + "currentRMS": "Current RMS", + "currentdbFS": "Current dbFS" + }, "boundingBoxes": { "title": "Bounding boxes", "desc": "Show bounding boxes around tracked objects", @@ -458,7 +459,6 @@ "title": "Object Shape Filter Drawing", "desc": "Draw a rectangle on the image to view area and ratio details", "tips": "Enable this option to draw a rectangle on the camera image to show its area and ratio. These values can then be used to set object shape filter parameters in your config.", - "document": "Read the documentation ", "score": "Score", "ratio": "Ratio", "area": "Area" @@ -565,13 +565,11 @@ "title": "Notifications", "notificationSettings": { "title": "Notification Settings", - "desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA.", - "documentation": "Read the Documentation" + "desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA." }, "notificationUnavailable": { "title": "Notifications Unavailable", - "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications.", - "documentation": "Read the Documentation" + "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications." }, "globalSettings": { "title": "Global Settings", @@ -628,7 +626,6 @@ "snapshotConfig": { "title": "Snapshot Configuration", "desc": "Submitting to Frigate+ requires both snapshots and clean_copy snapshots to be enabled in your config.", - "documentation": "Read the documentation", "cleanCopyWarning": "Some cameras have snapshots enabled but have the clean copy disabled. You need to enable clean_copy in your snapshot config to be able to submit images from these cameras to Frigate+.", "table": { "camera": "Camera", diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index 059f05f9f..1357000c9 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -91,6 +91,10 @@ "tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.", "earliestRecording": "Earliest recording available:" }, + "shm": { + "title": "SHM (shared memory) allocation", + "warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB." + }, "cameraStorage": { "title": "Camera Storage", "camera": "Camera", diff --git a/web/public/locales/es/common.json b/web/public/locales/es/common.json index bf6a735fa..7d34c8521 100644 --- a/web/public/locales/es/common.json +++ b/web/public/locales/es/common.json @@ -141,7 +141,14 @@ "fr": "Français (Frances)", "yue": "粵語 (Cantonés)", "th": "ไทย (Tailandés)", - "ca": "Català (Catalan)" + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portugués brasileño)", + "sr": "Српски (Serbio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Gallego)", + "id": "Bahasa Indonesia (Indonesio)" }, "appearance": "Apariencia", "darkMode": { @@ -271,5 +278,6 @@ "title": "404", "desc": "Página no encontrada" }, - "selectItem": "Seleccionar {{item}}" + "selectItem": "Seleccionar {{item}}", + "readTheDocumentation": "Leer la documentación" } diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json index 6950c7999..c57237ca4 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -178,6 +178,43 @@ "streams": { "title": "Transmisiones", "desc": "Desactivar temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de esta cámara por parte de Frigate. La detección, grabación y depuración no estarán disponibles.
Nota: Esto no desactiva las retransmisiones de go2rtc." + }, + "object_descriptions": { + "title": "Descripciones de objetos de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de objetos de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los objetos rastreados en esta cámara." + }, + "review_descriptions": { + "title": "Descripciones de revisión de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de revisión de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los elementos de revisión en esta cámara." + }, + "addCamera": "Añadir nueva cámara", + "editCamera": "Editar cámara:", + "selectCamera": "Seleccionar una cámara", + "backToSettings": "Volver a la configuración de la cámara", + "cameraConfig": { + "add": "Añadir cámara", + "edit": "Editar cámara", + "description": "Configurar los ajustes de la cámara, incluyendo las entradas de flujo y los roles.", + "name": "Nombre de la cámara", + "nameRequired": "El nombre de la cámara es obligatorio", + "nameInvalid": "El nombre de la cámara debe contener solo letras, números, guiones bajos o guiones", + "namePlaceholder": "p. ej., puerta_principal", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Flujos de entrada", + "path": "Ruta del flujo", + "pathRequired": "La ruta del flujo es obligatoria", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "Se requiere al menos un rol", + "rolesUnique": "Cada rol (audio, detección, grabación) solo puede asignarse a un flujo", + "addInput": "Añadir flujo de entrada", + "removeInput": "Eliminar flujo de entrada", + "inputsRequired": "Se requiere al menos un flujo de entrada" + }, + "toast": { + "success": "Cámara {{cameraName}} guardada con éxito" + } } }, "masksAndZones": { @@ -423,6 +460,11 @@ "score": "Puntuación", "ratio": "Proporción", "area": "Área" + }, + "paths": { + "title": "Rutas", + "desc": "Mostrar puntos significativos de la ruta del objeto rastreado", + "tips": "

Rutas


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

" } }, "users": { @@ -683,5 +725,12 @@ "success": "Los ajustes de enriquecimientos se han guardado. Reinicia Frigate para aplicar los cambios.", "error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}" } + }, + "triggers": { + "documentTitle": "Disparadores", + "management": { + "title": "Gestión de disparadores", + "desc": "Gestionar disparadores para {{camera}}. Usa el tipo de miniatura para activar en miniaturas similares al objeto rastreado seleccionado, y el tipo de descripción para activar en descripciones similares al texto que especifiques." + } } } diff --git a/web/public/locales/fi/common.json b/web/public/locales/fi/common.json index f76eb0e67..4c749676a 100644 --- a/web/public/locales/fi/common.json +++ b/web/public/locales/fi/common.json @@ -168,5 +168,6 @@ "length": { "feet": "jalka" } - } + }, + "readTheDocumentation": "Lue dokumentaatio" } diff --git a/web/public/locales/fi/views/explore.json b/web/public/locales/fi/views/explore.json index c6950c941..e59fc5bc4 100644 --- a/web/public/locales/fi/views/explore.json +++ b/web/public/locales/fi/views/explore.json @@ -7,7 +7,36 @@ "desc": "Tarkastele kohteen tietoja", "button": { "share": "Jaa tämä tarkasteltu kohde" + }, + "toast": { + "error": { + "updatedSublabelFailed": "Alatunnisteen päivitys epäonnistui", + "updatedLPRFailed": "Rekisterikilven päivitys epäonnistui" + } } + }, + "recognizedLicensePlate": "Tunnistettu rekisterikilpi", + "estimatedSpeed": "Arvioitu nopeus", + "objects": "Objektit", + "camera": "Kamera", + "zones": "Alueet", + "label": "Tunniste", + "editSubLabel": { + "title": "Editoi alitunnistetta", + "desc": "Syötä uusi alitunniste tähän", + "descNoLabel": "Lisää uusi alatunniste tähän seurattuun kohteeseen" + }, + "editLPR": { + "title": "Muokkaa rekisterikilpeä", + "desc": "Syötä uusi rekisterikilven arvo tähän", + "descNoLabel": "Syötä uusi rekisterikilven arvo tähän seurattuun objektiin" + }, + "snapshotScore": { + "label": "Tilannekuvan arvosana" + }, + "topScore": { + "label": "Huippuarvosana", + "info": "Ylin pistemäärä on seurattavan kohteen korkein mediaani, joten tämä voi erota hakutuloksen esikatselukuvassa näkyvästä pistemäärästä." } }, "exploreIsUnavailable": { @@ -28,7 +57,8 @@ "setup": { "visionModel": "Vision-malli", "textModel": "Tekstimalli", - "textTokenizer": "Tekstin osioija" + "textTokenizer": "Tekstin osioija", + "visionModelFeatureExtractor": "Näkömallin piirreluokkain" }, "tips": { "documentation": "Lue dokumentaatio", diff --git a/web/public/locales/fr/common.json b/web/public/locales/fr/common.json index 5ed9f65a9..960dd1d0d 100644 --- a/web/public/locales/fr/common.json +++ b/web/public/locales/fr/common.json @@ -162,7 +162,15 @@ "vi": "Tiếng Việt (Vietnamien)", "yue": "粵語 (Cantonais)", "th": "ไทย (Thai)", - "ca": "Català (Catalan)" + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (portugais brésilien)", + "sr": "Српски (Serbe)", + "sl": "Slovenščina (slovène)", + "lt": "Lietuvių (lithuanien)", + "bg": "Български (bulgare)", + "gl": "Galego (galicien)", + "id": "Bahasa Indonesia (indonésien)", + "ur": "اردو (ourdou)" }, "appearance": "Apparence", "darkMode": { @@ -254,6 +262,7 @@ "desc": "Page non trouvée" }, "selectItem": "Sélectionner {{item}}", + "readTheDocumentation": "Lire la documentation", "accessDenied": { "title": "Accès refusé", "documentTitle": "Accès refusé - Frigate", diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json index d92e3ff72..111baf88c 100644 --- a/web/public/locales/fr/components/dialog.json +++ b/web/public/locales/fr/components/dialog.json @@ -122,5 +122,12 @@ "markAsReviewed": "Marquer comme passé en revue", "deleteNow": "Supprimer maintenant" } + }, + "imagePicker": { + "selectImage": "Sélectionnez une vignette d'objet suivi", + "search": { + "placeholder": "Chercher par libellé ou sous-libellé..." + }, + "noImages": "Aucune vignette trouvée pour cette caméra" } } diff --git a/web/public/locales/fr/components/filter.json b/web/public/locales/fr/components/filter.json index 567cf81f5..710e22e0e 100644 --- a/web/public/locales/fr/components/filter.json +++ b/web/public/locales/fr/components/filter.json @@ -123,5 +123,13 @@ }, "zoneMask": { "filterBy": "Filtrer par masque de zone" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Toutes les classes" + }, + "count_one": "{{count}} classe", + "count_other": "{{count}} classes" } } diff --git a/web/public/locales/fr/views/configEditor.json b/web/public/locales/fr/views/configEditor.json index 5f88fb94f..ce4c3292e 100644 --- a/web/public/locales/fr/views/configEditor.json +++ b/web/public/locales/fr/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Erreur lors de l'enregistrement de la configuration" } }, - "confirm": "Quitter sans enregistrer ?" + "confirm": "Quitter sans enregistrer ?", + "safeConfigEditor": "Éditeur de configuration (mode sans échec)", + "safeModeDescription": "Frigate est en mode sans échec en raison d'une erreur de validation de la configuration." } diff --git a/web/public/locales/fr/views/events.json b/web/public/locales/fr/views/events.json index d8d58332c..cf4f5e142 100644 --- a/web/public/locales/fr/views/events.json +++ b/web/public/locales/fr/views/events.json @@ -35,5 +35,7 @@ "selected": "{{count}} sélectionné(s)", "selected_other": "{{count}} sélectionné(s)", "selected_one": "{{count}} sélectionné(s)", - "detected": "détecté" + "detected": "détecté", + "suspiciousActivity": "Activité suspecte", + "threateningActivity": "Activité menaçante" } diff --git a/web/public/locales/fr/views/explore.json b/web/public/locales/fr/views/explore.json index b42cb5f38..064a71a37 100644 --- a/web/public/locales/fr/views/explore.json +++ b/web/public/locales/fr/views/explore.json @@ -41,12 +41,14 @@ "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." + "updatedLPR": "Plaque d'immatriculation mise à jour avec succès.", + "audioTranscription": "Requête de la transcription audio réussie." }, "error": { "regenerate": "Échec de l'appel de {{provider}} pour une nouvelle description : {{errorMessage}}", "updatedSublabelFailed": "Échec de la mise à jour du sous-libellé : {{errorMessage}}", - "updatedLPRFailed": "Échec de la mise à jour de la plaque d'immatriculation : {{errorMessage}}" + "updatedLPRFailed": "Échec de la mise à jour de la plaque d'immatriculation : {{errorMessage}}", + "audioTranscription": "Échec de la requête de transcription audio : {{errorMessage}}" } }, "tips": { @@ -98,6 +100,9 @@ }, "snapshotScore": { "label": "Score de l'instantané" + }, + "score": { + "label": "Score" } }, "type": { @@ -183,6 +188,14 @@ }, "deleteTrackedObject": { "label": "Supprimer cet objet suivi" + }, + "addTrigger": { + "label": "Ajouter un déclencheur", + "aria": "Ajouter un déclencheur pour cet objet suivi" + }, + "audioTranscription": { + "label": "Transcrire", + "aria": "Demander une transcription audio" } }, "dialog": { @@ -205,5 +218,11 @@ }, "tooltip": "Correspondance : {{type}} à {{confidence}}%" }, - "exploreMore": "Explorer plus d'objets {{label}}" + "exploreMore": "Explorer plus d'objets {{label}}", + "aiAnalysis": { + "title": "Analyse IA" + }, + "concerns": { + "label": "Préoccupations" + } } diff --git a/web/public/locales/fr/views/live.json b/web/public/locales/fr/views/live.json index 8c8603972..4f951be35 100644 --- a/web/public/locales/fr/views/live.json +++ b/web/public/locales/fr/views/live.json @@ -43,7 +43,15 @@ "label": "Cliquez dans le cadre pour centrer la caméra PTZ" } }, - "presets": "Paramètres prédéfinis pour les caméras PTZ" + "presets": "Paramètres prédéfinis pour les caméras PTZ", + "focus": { + "in": { + "label": "Mise au point rapprochée de la caméra PTZ" + }, + "out": { + "label": "Mise au point éloignée de la caméra PTZ" + } + } }, "camera": { "enable": "Activer la caméra", @@ -126,7 +134,8 @@ "audioDetection": "Détection audio", "autotracking": "Suivi automatique", "cameraEnabled": "Caméra activée", - "title": "Paramètres de {{camera}}" + "title": "Paramètres de {{camera}}", + "transcription": "Transcription audio" }, "history": { "label": "Afficher l'historique de capture" @@ -154,5 +163,9 @@ "label": "Modifier le groupe de caméras" }, "exitEdit": "Quitter l'édition" + }, + "transcription": { + "enable": "Activer la transcription audio en direct", + "disable": "Désactiver la transcription audio en direct" } } diff --git a/web/public/locales/fr/views/settings.json b/web/public/locales/fr/views/settings.json index 67bd50933..32b13cdde 100644 --- a/web/public/locales/fr/views/settings.json +++ b/web/public/locales/fr/views/settings.json @@ -278,6 +278,43 @@ "streams": { "title": "Flux", "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation complète d'une caméra interrompt le traitement des flux de cette caméra par Frigate. La détection, l'enregistrement et le débogage seront indisponibles.
Remarque : cela ne désactive pas les rediffusions go2rtc." + }, + "object_descriptions": { + "title": "Description d'objets par IA générative", + "desc": "Activer / désactiver temporairement les descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront pas demandées pour les objets suivis par cette caméra." + }, + "review_descriptions": { + "title": "Revue de descriptions par IA générative", + "desc": "Activer / désactiver temporairement la revue de descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront plus demandées pour la revue d'éléments de cette caméra." + }, + "addCamera": "Ajouter une nouvelle caméra", + "editCamera": "Éditer la caméra :", + "selectCamera": "Sélectionner une caméra", + "backToSettings": "Retour aux paramètres de la caméra", + "cameraConfig": { + "add": "Ajouter une caméra", + "edit": "Éditer la caméra", + "description": "Configurer les paramètres de la caméra y compris les flux et les rôles.", + "name": "Nom de la caméra", + "nameRequired": "Un nom de caméra est nécessaire", + "nameInvalid": "Les noms de caméra peuvent contenir uniquement des lettres, des chiffres, des tirets bas, ou des tirets", + "namePlaceholder": "par exemple, porte_entree", + "enabled": "Activé", + "ffmpeg": { + "inputs": "Flux entrants", + "path": "Chemin d'accès du flux", + "pathRequired": "Un chemin d'accès de flux est nécessaire", + "pathPlaceholder": "rtsp://...", + "roles": "Rôles", + "rolesRequired": "Au moins un rôle est nécessaire", + "rolesUnique": "Chaque rôle (audio, détection, enregistrement) ne peut être assigné qu'à un seul flux", + "addInput": "Ajouter un flux entrant", + "removeInput": "Supprimer le flux entrant", + "inputsRequired": "Au moins un flux entrant est nécessaire" + }, + "toast": { + "success": "Caméra {{cameraName}} enregistrée avec succès" + } } }, "masksAndZones": { @@ -523,7 +560,12 @@ "noObjects": "Aucun objet", "title": "Débogage", "detectorDesc": "Frigate utilise vos détecteurs ({{detectors}}) pour détecter les objets dans le flux vidéo de votre caméra.", - "desc": "La vue de débogage affiche en temps réel les objets suivis et leurs statistiques. La liste des objets affiche un résumé différé des objets détectés." + "desc": "La vue de débogage affiche en temps réel les objets suivis et leurs statistiques. La liste des objets affiche un résumé différé des objets détectés.", + "paths": { + "title": "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.

" + } }, "users": { "title": "Utilisateurs", @@ -683,5 +725,100 @@ "success": "Les paramètres de données augmentées ont été enregistrés. Redémarrez Frigate pour appliquer les modifications." }, "restart_required": "Redémarrage nécessaire (paramètres des données augmentées modifiés)" + }, + "triggers": { + "documentTitle": "Déclencheurs", + "management": { + "title": "Gestion des déclencheurs", + "desc": "Gérer les déclencheurs pour {{camera}}. Utilisez le type vignette pour déclencher à partir de vignettes similaires à l'objet suivi sélectionné. Utilisez le type description pour déclencher à partir de textes de description similaires que vous avez spécifiés." + }, + "addTrigger": "Ajouter un déclencheur", + "table": { + "name": "Nom", + "type": "Type", + "content": "Contenu", + "threshold": "Seuil", + "actions": "Actions", + "noTriggers": "Aucun déclencheur configuré pour cette caméra.", + "edit": "Éditer", + "deleteTrigger": "Supprimer le déclencheur", + "lastTriggered": "Dernier déclencheur" + }, + "type": { + "thumbnail": "Vignette", + "description": "Description" + }, + "actions": { + "alert": "Marquer comme alerte", + "notification": "Envoyer une notification" + }, + "dialog": { + "createTrigger": { + "title": "Créer un déclencheur", + "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}}" + }, + "deleteTrigger": { + "title": "Supprimer le déclencheur", + "desc": "Êtes-vous sûr de vouloir supprimer le déclencheur {{triggerName}} ? Cette action est irréversible." + }, + "form": { + "name": { + "title": "Nom", + "placeholder": "Entrez le nom du 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.", + "alreadyExists": "Un déclencheur avec le même nom existe déjà pour cette caméra." + } + }, + "enabled": { + "description": "Activer ou désactiver ce déclencheur" + }, + "type": { + "title": "Type", + "placeholder": "Sélectionner un type de déclencheur" + }, + "content": { + "title": "Contenu", + "imagePlaceholder": "Sélectionner une image", + "textPlaceholder": "Saisir le contenu du texte", + "imageDesc": "Sélectionnez une image pour déclencher cette action lorsqu'une image similaire est détectée.", + "textDesc": "Entrez un texte pour déclencher cette action lorsqu'une description similaire d'objet suivi est détectée.", + "error": { + "required": "Le contenu est requis." + } + }, + "threshold": { + "title": "Seuil", + "error": { + "min": "Le seuil doit être au moins 0", + "max": "Le seuil peut être au plus 1" + } + }, + "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.", + "error": { + "min": "Au moins une action doit être sélectionnée." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Le déclencheur {{name}} a été créé avec succès.", + "updateTrigger": "Le déclencheur {{name}} a été mis à jour avec succès.", + "deleteTrigger": "Le déclencheur {{name}} a été supprimé avec succès." + }, + "error": { + "createTriggerFailed": "Échec de la création du déclencheur : {{errorMessage}}", + "updateTriggerFailed": "Échec de la mise à jour du déclencheur : {{errorMessage}}", + "deleteTriggerFailed": "Échec de la suppression du déclencheur : {{errorMessage}}" + } + } } } diff --git a/web/public/locales/gl/common.json b/web/public/locales/gl/common.json index 1443cce35..61b9ce58f 100644 --- a/web/public/locales/gl/common.json +++ b/web/public/locales/gl/common.json @@ -9,5 +9,6 @@ "today": "Hoxe", "untilRestart": "Ata o reinicio", "ago": "Fai {{timeAgo}}" - } + }, + "readTheDocumentation": "Ler a documentación" } diff --git a/web/public/locales/gl/components/auth.json b/web/public/locales/gl/components/auth.json index 2a0bee0d5..8b0857dac 100644 --- a/web/public/locales/gl/components/auth.json +++ b/web/public/locales/gl/components/auth.json @@ -5,7 +5,8 @@ "errors": { "passwordRequired": "Contrasinal obrigatorio", "unknownError": "Erro descoñecido. Revisa os logs.", - "usernameRequired": "Usuario/a obrigatorio" + "usernameRequired": "Usuario/a obrigatorio", + "rateLimit": "Excedido o límite. Téntao de novo despois." }, "login": "Iniciar sesión" } diff --git a/web/public/locales/gl/components/dialog.json b/web/public/locales/gl/components/dialog.json index c6519972a..d2aff40d1 100644 --- a/web/public/locales/gl/components/dialog.json +++ b/web/public/locales/gl/components/dialog.json @@ -15,6 +15,9 @@ "label": "Confirma esta etiqueta para Frigate Plus", "ask_an": "E isto un obxecto {{label}}?" } + }, + "submitToPlus": { + "label": "Enviar a Frigate+" } } } diff --git a/web/public/locales/gl/components/filter.json b/web/public/locales/gl/components/filter.json index 6927e2e51..8ef5f8fd1 100644 --- a/web/public/locales/gl/components/filter.json +++ b/web/public/locales/gl/components/filter.json @@ -6,7 +6,8 @@ "all": { "short": "Etiquetas", "title": "Todas as Etiquetas" - } + }, + "count_other": "{{count}} Etiquetas" }, "zones": { "all": { diff --git a/web/public/locales/gl/views/events.json b/web/public/locales/gl/views/events.json index c5c9cb67b..56da9d9e5 100644 --- a/web/public/locales/gl/views/events.json +++ b/web/public/locales/gl/views/events.json @@ -6,5 +6,8 @@ "motion": { "only": "Só movemento", "label": "Movemento" + }, + "empty": { + "alert": "Non hai alertas que revisar" } } diff --git a/web/public/locales/he/common.json b/web/public/locales/he/common.json index e6c1d632f..4a89aba19 100644 --- a/web/public/locales/he/common.json +++ b/web/public/locales/he/common.json @@ -261,5 +261,6 @@ "title": "404", "desc": "דף לא נמצא" }, - "selectItem": "בחירה:{{item}}" + "selectItem": "בחירה:{{item}}", + "readTheDocumentation": "קרא את התיעוד" } diff --git a/web/public/locales/hi/audio.json b/web/public/locales/hi/audio.json index afffaf44a..0705110cf 100644 --- a/web/public/locales/hi/audio.json +++ b/web/public/locales/hi/audio.json @@ -139,5 +139,7 @@ "raindrop": "बारिश की बूंद", "rowboat": "चप्पू वाली नाव", "aircraft": "विमान", - "bicycle": "साइकिल" + "bicycle": "साइकिल", + "bellow": "गर्जना करना", + "motorcycle": "मोटरसाइकिल" } diff --git a/web/public/locales/hi/common.json b/web/public/locales/hi/common.json index 392c9a844..d4c433519 100644 --- a/web/public/locales/hi/common.json +++ b/web/public/locales/hi/common.json @@ -2,6 +2,7 @@ "time": { "untilForTime": "{{time}} तक", "untilForRestart": "जब तक फ्रिगेट पुनः रीस्टार्ट नहीं हो जाता।", - "untilRestart": "रीस्टार्ट होने में" + "untilRestart": "रीस्टार्ट होने में", + "ago": "{{timeAgo}} पहले" } } diff --git a/web/public/locales/hi/components/camera.json b/web/public/locales/hi/components/camera.json index 37c5b27ed..74c05d469 100644 --- a/web/public/locales/hi/components/camera.json +++ b/web/public/locales/hi/components/camera.json @@ -2,6 +2,9 @@ "group": { "label": "कैमरा समूह", "add": "कैमरा समूह जोड़ें", - "edit": "कैमरा समूह संपादित करें" + "edit": "कैमरा समूह संपादित करें", + "delete": { + "label": "कैमरा समूह हटाएँ" + } } } diff --git a/web/public/locales/hi/components/dialog.json b/web/public/locales/hi/components/dialog.json index dce6983b5..bcf4cd070 100644 --- a/web/public/locales/hi/components/dialog.json +++ b/web/public/locales/hi/components/dialog.json @@ -3,7 +3,8 @@ "title": "क्या आप निश्चित हैं कि आप फ्रिगेट को रीस्टार्ट करना चाहते हैं?", "button": "रीस्टार्ट", "restarting": { - "title": "फ्रिगेट रीस्टार्ट हो रहा है" + "title": "फ्रिगेट रीस्टार्ट हो रहा है", + "content": "यह पृष्ठ {{countdown}} सेकंड में पुनः लोड होगा।" } } } diff --git a/web/public/locales/hi/components/filter.json b/web/public/locales/hi/components/filter.json index 214179375..a89133b70 100644 --- a/web/public/locales/hi/components/filter.json +++ b/web/public/locales/hi/components/filter.json @@ -3,7 +3,8 @@ "labels": { "label": "लेबल", "all": { - "title": "सभी लेबल" + "title": "सभी लेबल", + "short": "लेबल" } } } diff --git a/web/public/locales/hi/components/player.json b/web/public/locales/hi/components/player.json index 9b4ed4389..e5e63a82d 100644 --- a/web/public/locales/hi/components/player.json +++ b/web/public/locales/hi/components/player.json @@ -1,5 +1,8 @@ { "noRecordingsFoundForThisTime": "इस समय का कोई रिकॉर्डिंग नहीं मिला", "noPreviewFound": "कोई प्रीव्यू नहीं मिला", - "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला" + "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला", + "submitFrigatePlus": { + "title": "इस फ्रेम को फ्रिगेट+ पर सबमिट करें?" + } } diff --git a/web/public/locales/hi/objects.json b/web/public/locales/hi/objects.json index 436a57668..a4e93c3ab 100644 --- a/web/public/locales/hi/objects.json +++ b/web/public/locales/hi/objects.json @@ -13,5 +13,6 @@ "vehicle": "वाहन", "car": "गाड़ी", "person": "व्यक्ति", - "bicycle": "साइकिल" + "bicycle": "साइकिल", + "motorcycle": "मोटरसाइकिल" } diff --git a/web/public/locales/hi/views/events.json b/web/public/locales/hi/views/events.json index b6fba2aa1..ae2091467 100644 --- a/web/public/locales/hi/views/events.json +++ b/web/public/locales/hi/views/events.json @@ -1,4 +1,8 @@ { "alerts": "अलर्टस", - "detections": "खोजें" + "detections": "खोजें", + "motion": { + "label": "गति", + "only": "केवल गति" + } } diff --git a/web/public/locales/hi/views/explore.json b/web/public/locales/hi/views/explore.json index bb214ba12..daafb9c2d 100644 --- a/web/public/locales/hi/views/explore.json +++ b/web/public/locales/hi/views/explore.json @@ -1,4 +1,8 @@ { "documentTitle": "अन्वेषण करें - फ्रिगेट", - "generativeAI": "जनरेटिव ए आई" + "generativeAI": "जनरेटिव ए आई", + "exploreMore": "और अधिक {{label}} वस्तुओं का अन्वेषण करें", + "exploreIsUnavailable": { + "title": "अन्वेषण अनुपलब्ध है" + } } diff --git a/web/public/locales/hi/views/exports.json b/web/public/locales/hi/views/exports.json index 97a0f0e53..b9e86dac1 100644 --- a/web/public/locales/hi/views/exports.json +++ b/web/public/locales/hi/views/exports.json @@ -1,4 +1,6 @@ { "documentTitle": "निर्यात - फ्रिगेट", - "search": "खोजें" + "search": "खोजें", + "noExports": "कोई निर्यात नहीं मिला", + "deleteExport": "निर्यात हटाएँ" } diff --git a/web/public/locales/hi/views/faceLibrary.json b/web/public/locales/hi/views/faceLibrary.json index 5c8de952e..b30528265 100644 --- a/web/public/locales/hi/views/faceLibrary.json +++ b/web/public/locales/hi/views/faceLibrary.json @@ -1,6 +1,7 @@ { "description": { "addFace": "फेस लाइब्रेरी में नया संग्रह जोड़ने की प्रक्रिया को आगे बढ़ाएं।", - "placeholder": "इस संग्रह का नाम बताएं" + "placeholder": "इस संग्रह का नाम बताएं", + "invalidName": "अमान्य नाम. नाम में केवल अक्षर, संख्याएँ, रिक्त स्थान, एपॉस्ट्रॉफ़ी, अंडरस्कोर और हाइफ़न ही शामिल हो सकते हैं।" } } diff --git a/web/public/locales/hi/views/live.json b/web/public/locales/hi/views/live.json index 86d2a9235..9c7edbeab 100644 --- a/web/public/locales/hi/views/live.json +++ b/web/public/locales/hi/views/live.json @@ -1,4 +1,5 @@ { "documentTitle": "लाइव - फ्रिगेट", - "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट" + "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट", + "lowBandwidthMode": "कम बैंडविड्थ मोड" } diff --git a/web/public/locales/hi/views/search.json b/web/public/locales/hi/views/search.json index b38dd11af..2ea0c8cfe 100644 --- a/web/public/locales/hi/views/search.json +++ b/web/public/locales/hi/views/search.json @@ -1,4 +1,5 @@ { "search": "खोजें", - "savedSearches": "सहेजी गई खोजें" + "savedSearches": "सहेजी गई खोजें", + "searchFor": "{{inputValue}} खोजें" } diff --git a/web/public/locales/hi/views/settings.json b/web/public/locales/hi/views/settings.json index 5fe3a3233..d9bf27ffc 100644 --- a/web/public/locales/hi/views/settings.json +++ b/web/public/locales/hi/views/settings.json @@ -1,6 +1,7 @@ { "documentTitle": { "default": "सेटिंग्स - फ्रिगेट", - "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट" + "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट", + "camera": "कैमरा सेटिंग्स - फ्रिगेट" } } diff --git a/web/public/locales/hi/views/system.json b/web/public/locales/hi/views/system.json index b29ff9abb..23bafa3fc 100644 --- a/web/public/locales/hi/views/system.json +++ b/web/public/locales/hi/views/system.json @@ -1,6 +1,7 @@ { "documentTitle": { "cameras": "कैमरा आँकड़े - फ्रिगेट", - "storage": "भंडारण आँकड़े - फ्रिगेट" + "storage": "भंडारण आँकड़े - फ्रिगेट", + "general": "सामान्य आँकड़े - फ्रिगेट" } } diff --git a/web/public/locales/hu/audio.json b/web/public/locales/hu/audio.json index 7d5d49bf9..cc73f3ccc 100644 --- a/web/public/locales/hu/audio.json +++ b/web/public/locales/hu/audio.json @@ -149,7 +149,7 @@ "car": "Autó", "bus": "Busz", "motorcycle": "Motor", - "train": "Vonat", + "train": "Betanít", "bicycle": "Bicikli", "scream": "Sikoly", "throat_clearing": "Torokköszörülés", diff --git a/web/public/locales/hu/common.json b/web/public/locales/hu/common.json index c45157b38..f75193fd1 100644 --- a/web/public/locales/hu/common.json +++ b/web/public/locales/hu/common.json @@ -142,7 +142,15 @@ "ro": "Román", "hu": "Magyar", "fi": "Finn", - "th": "Thai" + "th": "Thai", + "ptBR": "Português brasileiro (Brazil portugál)", + "sr": "Српски (Szerb)", + "sl": "Slovenščina (Szlovén)", + "lt": "Lietuvių (Litván)", + "bg": "Български (Bolgár)", + "gl": "Galego (Galíciai)", + "id": "Bahasa Indonesia (Indonéz)", + "ur": "اردو (Urdu)" }, "uiPlayground": "UI játszótér", "faceLibrary": "Arc Könyvtár", @@ -254,5 +262,6 @@ }, "label": { "back": "Vissza" - } + }, + "readTheDocumentation": "Olvassa el a dokumentációt" } diff --git a/web/public/locales/hu/components/camera.json b/web/public/locales/hu/components/camera.json index e53fb9c18..d27cc1282 100644 --- a/web/public/locales/hu/components/camera.json +++ b/web/public/locales/hu/components/camera.json @@ -1,6 +1,6 @@ { "group": { - "label": "Kamera Csoport", + "label": "Kamera Csoportok", "delete": { "confirm": { "desc": "Biztosan törölni akarja a következő kamera csoportot {{name}}?", diff --git a/web/public/locales/hu/components/dialog.json b/web/public/locales/hu/components/dialog.json index bff6ade19..46664bda8 100644 --- a/web/public/locales/hu/components/dialog.json +++ b/web/public/locales/hu/components/dialog.json @@ -1,6 +1,6 @@ { "restart": { - "title": "Biztosan újra szeretnéd indítani a Frigate-ot?", + "title": "Biztosan újra szeretnéd indítani a Frigate-et?", "button": "Újraindítás", "restarting": { "title": "A Frigate újraindul", @@ -22,7 +22,7 @@ "ask_a": "Ez a tárgy egy {{label}}?", "label": "Erősítse meg ezt a cimkét a Frigate plus felé", "ask_an": "Ez a tárgy egy {{label}}?", - "ask_full": "Ez a tárgy egy {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Ez a tárgy egy {{translatedLabel}} ({{untranslatedLabel}})?" } } }, @@ -109,5 +109,11 @@ "deleteNow": "Törlés Most", "export": "Exportálás" } + }, + "imagePicker": { + "selectImage": "Válassza ki egy követett tárgy képét", + "search": { + "placeholder": "Keresés cimke vagy alcimke alapján..." + } } } diff --git a/web/public/locales/hu/components/filter.json b/web/public/locales/hu/components/filter.json index f4b9b9f39..fcff467c9 100644 --- a/web/public/locales/hu/components/filter.json +++ b/web/public/locales/hu/components/filter.json @@ -122,5 +122,13 @@ "selectPlatesFromList": "Válasszon ki egy vagy több rendszámtáblát a listából.", "loading": "Felismert rendszámtáblák betöltése…", "placeholder": "Kezdjen gépelni a rendszámok közötti kereséshez…" + }, + "classes": { + "label": "Osztályok", + "all": { + "title": "Minden Osztály" + }, + "count_one": "{{count}} Osztály", + "count_other": "{{count}} Osztályok" } } diff --git a/web/public/locales/hu/objects.json b/web/public/locales/hu/objects.json index 5bac94fc3..4b53d161b 100644 --- a/web/public/locales/hu/objects.json +++ b/web/public/locales/hu/objects.json @@ -5,7 +5,7 @@ "motorcycle": "Motor", "airplane": "Repülőgép", "bus": "Busz", - "train": "Vonat", + "train": "Betanít", "boat": "Hajó", "dog": "Kutya", "cat": "Macska", diff --git a/web/public/locales/hu/views/configEditor.json b/web/public/locales/hu/views/configEditor.json index b921c987d..69fa822e9 100644 --- a/web/public/locales/hu/views/configEditor.json +++ b/web/public/locales/hu/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Hiba a konfiguráció mentésekor" } }, - "confirm": "Kilép mentés nélkül?" + "confirm": "Kilép mentés nélkül?", + "safeConfigEditor": "Konfiguráció szerkesztő (Biztosnági Mód)", + "safeModeDescription": "Frigate biztonsági módban van konfigurációs hiba miatt." } diff --git a/web/public/locales/hu/views/explore.json b/web/public/locales/hu/views/explore.json index 9f5cd4814..58354a27a 100644 --- a/web/public/locales/hu/views/explore.json +++ b/web/public/locales/hu/views/explore.json @@ -27,6 +27,10 @@ "downloadSnapshot": { "aria": "Pillanatfelvétel letöltése", "label": "Pillanatfelvétel letöltése" + }, + "addTrigger": { + "label": "Indító hozzáadása", + "aria": "Indító hozzáadása ehhez a követett tárgyhoz" } }, "details": { diff --git a/web/public/locales/hu/views/faceLibrary.json b/web/public/locales/hu/views/faceLibrary.json index 4aaef392d..7b12521d5 100644 --- a/web/public/locales/hu/views/faceLibrary.json +++ b/web/public/locales/hu/views/faceLibrary.json @@ -1,7 +1,7 @@ { "renameFace": { "title": "Arc átnevezése", - "desc": "Adjon meg egy új nevet {{name}}-nak/-nek" + "desc": "Adjon meg egy új nevet neki: {{name}}" }, "details": { "subLabelScore": "Alcimke érték", @@ -90,7 +90,7 @@ "nofaces": "Nincs elérhető arc", "documentTitle": "Arc könyvtár - Frigate", "train": { - "title": "Vonat", + "title": "Tanít", "empty": "Nincs friss arcfelismerés", "aria": "Válassza ki a tanítást" }, diff --git a/web/public/locales/hu/views/live.json b/web/public/locales/hu/views/live.json index 73a8f81f9..f26acd219 100644 --- a/web/public/locales/hu/views/live.json +++ b/web/public/locales/hu/views/live.json @@ -135,7 +135,8 @@ "recording": "Felvétel", "audioDetection": "Hang Észlelés", "snapshots": "Pillanatképek", - "autotracking": "Automatikus követés" + "autotracking": "Automatikus követés", + "transcription": "Hang Feliratozás" }, "history": { "label": "Előzmény felvételek megjelenítése" @@ -154,5 +155,9 @@ "label": "Kameracsoport szerkesztése" }, "exitEdit": "Szerkesztés bezárása" + }, + "transcription": { + "enable": "Élő Audio Feliratozás Engedélyezése", + "disable": "Élő Audio Feliratozás Kikapcsolása" } } diff --git a/web/public/locales/hu/views/settings.json b/web/public/locales/hu/views/settings.json index 5fc972304..056bb5ca6 100644 --- a/web/public/locales/hu/views/settings.json +++ b/web/public/locales/hu/views/settings.json @@ -616,6 +616,19 @@ "success": "Az Ellenőrzési Kategorizálás beállításai elmentésre kerültek. A módosítások alkalmazásához indítsa újra a Frigate-et." } }, - "title": "Kamera Beállítások" + "title": "Kamera Beállítások", + "object_descriptions": { + "title": "Generatív AI Tárgy Leírások" + }, + "addCamera": "Új Kamera Hozzáadása", + "editCamera": "Kamera Szerkesztése:", + "selectCamera": "Válasszon ki egy Kamerát", + "backToSettings": "Vissza a Kamera Beállításokhoz", + "cameraConfig": { + "add": "Kamera Hozzáadása", + "edit": "Kamera Szerkesztése", + "name": "Kamera Neve", + "nameRequired": "Kamera nevének megadása szükséges" + } } } diff --git a/web/public/locales/id/audio.json b/web/public/locales/id/audio.json index 0d46db1e1..d065bf137 100644 --- a/web/public/locales/id/audio.json +++ b/web/public/locales/id/audio.json @@ -27,5 +27,59 @@ "bicycle": "Sepeda", "bus": "Bis", "train": "Kereta", - "boat": "Kapal" + "boat": "Kapal", + "sneeze": "Bersin", + "run": "Lari", + "footsteps": "Langkah kaki", + "chewing": "Mengunyah", + "biting": "Menggigit", + "stomach_rumble": "Perut Keroncongan", + "burping": "Sendawa", + "hiccup": "Cegukan", + "fart": "Kentut", + "hands": "Tangan", + "heartbeat": "Detak Jantung", + "applause": "Tepuk Tangan", + "chatter": "Obrolan", + "children_playing": "Anak-Anak Bermain", + "animal": "Binatang", + "pets": "Peliharaan", + "dog": "Anjing", + "bark": "Gonggongan", + "howl": "Melolong", + "cat": "Kucing", + "meow": "Meong", + "livestock": "Hewan Ternak", + "horse": "Kuda", + "cattle": "Sapi", + "pig": "Babi", + "goat": "Kambing", + "sheep": "Domba", + "chicken": "Ayam", + "cluck": "Berkokok", + "cock_a_doodle_doo": "Kukuruyuk", + "turkey": "Kalkun", + "duck": "Bebek", + "quack": "Kwek", + "goose": "Angsa", + "wild_animals": "Hewan Liar", + "bird": "Burung", + "pigeon": "Merpati", + "crow": "Gagak", + "owl": "Burung Hantu", + "flapping_wings": "Kepakan Sayap", + "dogs": "Anjing", + "insect": "Serangga", + "cricket": "Jangkrik", + "mosquito": "Nyamuk", + "fly": "Lalat", + "frog": "Katak", + "snake": "Ular", + "music": "Musik", + "musical_instrument": "Alat Musik", + "guitar": "Gitar", + "electric_guitar": "Gitar Elektrik", + "acoustic_guitar": "Gitar Akustik", + "strum": "Genjreng", + "banjo": "Banjo" } diff --git a/web/public/locales/id/common.json b/web/public/locales/id/common.json index afe3a285c..9072354dc 100644 --- a/web/public/locales/id/common.json +++ b/web/public/locales/id/common.json @@ -10,5 +10,6 @@ "last7": "7 hari terakhir", "last14": "14 hari terakhir", "last30": "30 hari terakhir" - } + }, + "readTheDocumentation": "Baca dokumentasi" } diff --git a/web/public/locales/id/components/camera.json b/web/public/locales/id/components/camera.json index da128850f..9da7f9f2d 100644 --- a/web/public/locales/id/components/camera.json +++ b/web/public/locales/id/components/camera.json @@ -15,7 +15,8 @@ "placeholder": "Masukkan nama…", "errorMessage": { "mustLeastCharacters": "Nama grup kamera minimal harus 2 karakter.", - "exists": "Nama grup kamera sudah ada." + "exists": "Nama grup kamera sudah ada.", + "nameMustNotPeriod": "Nama grup kamera tidak boleh ada titik." } } } diff --git a/web/public/locales/id/objects.json b/web/public/locales/id/objects.json index ce7f18a78..bfeeca8ea 100644 --- a/web/public/locales/id/objects.json +++ b/web/public/locales/id/objects.json @@ -8,5 +8,13 @@ "train": "Kereta", "boat": "Kapal", "traffic_light": "Lampu Lalu Lintas", - "fire_hydrant": "Hidran Kebakaran" + "fire_hydrant": "Hidran Kebakaran", + "animal": "Binatang", + "dog": "Anjing", + "bark": "Gonggongan", + "cat": "Kucing", + "horse": "Kuda", + "goat": "Kambing", + "sheep": "Domba", + "bird": "Burung" } diff --git a/web/public/locales/id/views/configEditor.json b/web/public/locales/id/views/configEditor.json index 871c35180..3577999ab 100644 --- a/web/public/locales/id/views/configEditor.json +++ b/web/public/locales/id/views/configEditor.json @@ -12,5 +12,6 @@ "savingError": "Gagal menyimpan konfigurasi" } }, - "confirm": "Keluar tanpa menyimpan?" + "confirm": "Keluar tanpa menyimpan?", + "safeModeDescription": "Frigate sedang dalam mode aman karena kesalahan validasi konfigurasi." } diff --git a/web/public/locales/it/common.json b/web/public/locales/it/common.json index 9e0cb2e77..f4ee9086f 100644 --- a/web/public/locales/it/common.json +++ b/web/public/locales/it/common.json @@ -181,7 +181,15 @@ "it": "Italiano (Italiano)", "yue": "粵語 (Cantonese)", "th": "ไทย (Tailandese)", - "ca": "Català (Catalano)" + "ca": "Català (Catalano)", + "ptBR": "Português brasileiro (Portoghese brasiliano)", + "sr": "Српски (Serbo)", + "sl": "Slovenščina (Sloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Bulgaro)", + "gl": "Galego (Galiziano)", + "id": "Bahasa Indonesia (Indonesiano)", + "ur": "اردو (Urdu)" }, "darkMode": { "label": "Modalità scura", @@ -271,5 +279,6 @@ "title": "Salva" } }, - "selectItem": "Seleziona {{item}}" + "selectItem": "Seleziona {{item}}", + "readTheDocumentation": "Leggi la documentazione" } diff --git a/web/public/locales/it/components/camera.json b/web/public/locales/it/components/camera.json index 830cfd0e8..effe87147 100644 --- a/web/public/locales/it/components/camera.json +++ b/web/public/locales/it/components/camera.json @@ -29,7 +29,7 @@ "label": "Metodo di trasmissione", "method": { "smartStreaming": { - "label": "Trasmissione intelligente (consigliato)", + "label": "Trasmissione intelligente (consigliata)", "desc": "La trasmissione intelligente aggiorna l'immagine della telecamera una volta al minuto quando non si verifica alcuna attività rilevabile, per risparmiare larghezza di banda e risorse. Quando viene rilevata un'attività, l'immagine passa automaticamente alla trasmissione dal vivo." }, "continuousStreaming": { diff --git a/web/public/locales/it/components/dialog.json b/web/public/locales/it/components/dialog.json index 683d4ce2f..f5a124779 100644 --- a/web/public/locales/it/components/dialog.json +++ b/web/public/locales/it/components/dialog.json @@ -122,5 +122,12 @@ "error": "Impossibile eliminare: {{error}}" } } + }, + "imagePicker": { + "selectImage": "Seleziona la miniatura di un oggetto tracciato", + "search": { + "placeholder": "Cerca per etichetta o sottoetichetta..." + }, + "noImages": "Nessuna miniatura trovata per questa fotocamera" } } diff --git a/web/public/locales/it/components/filter.json b/web/public/locales/it/components/filter.json index dd6ebc1b7..1ca559818 100644 --- a/web/public/locales/it/components/filter.json +++ b/web/public/locales/it/components/filter.json @@ -122,5 +122,13 @@ }, "zoneMask": { "filterBy": "Filtra per maschera di zona" + }, + "classes": { + "label": "Classi", + "all": { + "title": "Tutte le classi" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classi" } } diff --git a/web/public/locales/it/views/configEditor.json b/web/public/locales/it/views/configEditor.json index 4ce1a7378..f53aaed58 100644 --- a/web/public/locales/it/views/configEditor.json +++ b/web/public/locales/it/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Errore durante il salvataggio della configurazione" } }, - "confirm": "Vuoi uscire senza salvare?" + "confirm": "Vuoi uscire senza salvare?", + "safeConfigEditor": "Editor di configurazione (modalità provvisoria)", + "safeModeDescription": "Frigate è in modalità provvisoria a causa di un errore di convalida della configurazione." } diff --git a/web/public/locales/it/views/events.json b/web/public/locales/it/views/events.json index e07c7bc6a..791bcc135 100644 --- a/web/public/locales/it/views/events.json +++ b/web/public/locales/it/views/events.json @@ -1,9 +1,9 @@ { "alerts": "Avvisi", - "detections": "Rilevamento", + "detections": "Rilevamenti", "motion": { - "label": "Movimento", - "only": "Solo movimento" + "label": "Movimenti", + "only": "Solo movimenti" }, "empty": { "alert": "Non ci sono avvisi da rivedere", @@ -35,5 +35,7 @@ "selected": "{{count}} selezionati", "selected_one": "{{count}} selezionati", "selected_other": "{{count}} selezionati", - "detected": "rilevato" + "detected": "rilevato", + "suspiciousActivity": "Attività sospetta", + "threateningActivity": "Attività minacciosa" } diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json index 547c6ad0a..a440e0281 100644 --- a/web/public/locales/it/views/explore.json +++ b/web/public/locales/it/views/explore.json @@ -52,12 +52,14 @@ "success": { "regenerate": "È stata richiesta una nuova descrizione a {{provider}}. A seconda della velocità del tuo provider, la rigenerazione della nuova descrizione potrebbe richiedere del tempo.", "updatedSublabel": "Sottoetichetta aggiornata correttamente.", - "updatedLPR": "Targa aggiornata con successo." + "updatedLPR": "Targa aggiornata con successo.", + "audioTranscription": "Trascrizione audio richiesta con successo." }, "error": { "regenerate": "Impossibile chiamare {{provider}} per una nuova descrizione: {{errorMessage}}", "updatedSublabelFailed": "Impossibile aggiornare la sottoetichetta: {{errorMessage}}", - "updatedLPRFailed": "Impossibile aggiornare la targa: {{errorMessage}}" + "updatedLPRFailed": "Impossibile aggiornare la targa: {{errorMessage}}", + "audioTranscription": "Impossibile richiedere la trascrizione audio: {{errorMessage}}" } } }, @@ -98,6 +100,9 @@ "tips": { "descriptionSaved": "Descrizione salvata correttamente", "saveDescriptionFailed": "Impossibile aggiornare la descrizione: {{errorMessage}}" + }, + "score": { + "label": "Punteggio" } }, "objectLifecycle": { @@ -182,6 +187,14 @@ "submitToPlus": { "label": "Invia a Frigate+", "aria": "Invia a Frigate Plus" + }, + "addTrigger": { + "label": "Aggiungi innesco", + "aria": "Aggiungi un innesco per questo oggetto tracciato" + }, + "audioTranscription": { + "label": "Trascrivere", + "aria": "Richiedi la trascrizione audio" } }, "dialog": { @@ -205,5 +218,11 @@ "trackedObjectsCount_other": "{{count}} oggetti tracciati ", "fetchingTrackedObjectsFailed": "Errore durante il recupero degli oggetti tracciati: {{errorMessage}}", "noTrackedObjects": "Nessun oggetto tracciato trovato", - "exploreMore": "Esplora altri oggetti {{label}}" + "exploreMore": "Esplora altri oggetti {{label}}", + "aiAnalysis": { + "title": "Analisi IA" + }, + "concerns": { + "label": "Preoccupazioni" + } } diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json index b8a44ae27..b264fecb7 100644 --- a/web/public/locales/it/views/live.json +++ b/web/public/locales/it/views/live.json @@ -37,7 +37,8 @@ "cameraEnabled": "Telecamera abilitata", "objectDetection": "Rilevamento di oggetti", "recording": "Registrazione", - "audioDetection": "Rilevamento audio" + "audioDetection": "Rilevamento audio", + "transcription": "Trascrizione audio" }, "history": { "label": "Mostra filmati storici" @@ -82,7 +83,15 @@ "label": "Fai clic nella cornice per centrare la telecamera PTZ" } }, - "presets": "Preimpostazioni della telecamera PTZ" + "presets": "Preimpostazioni della telecamera PTZ", + "focus": { + "in": { + "label": "Aumenta fuoco della telecamera PTZ" + }, + "out": { + "label": "Diminuisci fuoco della telecamera PTZ" + } + } }, "camera": { "enable": "Abilita telecamera", @@ -154,5 +163,9 @@ "label": "Modifica gruppo telecamere" }, "exitEdit": "Esci dalla modifica" + }, + "transcription": { + "enable": "Abilita la trascrizione audio in tempo reale", + "disable": "Disabilita la trascrizione audio in tempo reale" } } diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json index 78b74c3b4..b6c832a4c 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -18,7 +18,7 @@ "table": { "snapshots": "Istantanee", "camera": "Telecamera", - "cleanCopySnapshots": "clean_copy Istantanee" + "cleanCopySnapshots": "Istantanee clean_copy" }, "desc": "Per inviare a Frigate+ è necessario che nella configurazione siano abilitate sia le istantanee che le istantanee clean_copy.", "documentation": "Leggi la documentazione", @@ -101,6 +101,11 @@ "zones": { "title": "Zone", "desc": "Mostra un contorno di tutte le zone definite" + }, + "paths": { + "title": "Percorsi", + "desc": "Mostra i punti significativi del percorso dell'oggetto tracciato", + "tips": "

Percorsi


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

" } }, "masksAndZones": { @@ -292,7 +297,7 @@ "regardlessOfZoneObjectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano." }, "title": "Classificazione della revisione", - "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e auto sono considerati Avvisi. Puoi perfezionare la categorizzazione degli elementi di revisione configurando le zone desiderate.", + "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e automobile sono considerati Avvisi. Puoi perfezionare la categorizzazione degli elementi di revisione configurando le zone desiderate.", "objectAlertsTips": "Tutti gli oggetti {{alertsLabels}} su {{cameraName}} verranno mostrati come Avvisi.", "toast": { "success": "La configurazione della classificazione di revisione è stata salvata. Riavvia Frigate per applicare le modifiche." @@ -305,7 +310,7 @@ "unsavedChanges": "Impostazioni di classificazione delle revisioni non salvate per {{camera}}" }, "streams": { - "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili.
Nota: questa operazione non disabilita le ritrasmissioni di go2rtc.", + "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi da parte di Frigate. Rilevamenti, registrazioni e correzioni non saranno disponibili.
Nota: questa operazione non disabilita le ritrasmissioni di go2rtc.", "title": "Flussi" }, "title": "Impostazioni telecamera", @@ -314,6 +319,43 @@ "desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitati, non verranno generati nuovi elementi di revisione. ", "alerts": "Avvisi ", "detections": "Rilevamenti " + }, + "object_descriptions": { + "title": "Descrizioni di oggetti di IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera." + }, + "review_descriptions": { + "title": "Descrizioni delle revisioni dell'IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni delle revisioni dell'IA generativa per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli elementi da recensire su questa telecamera." + }, + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "cameraConfig": { + "add": "Aggiungi telecamera", + "edit": "Modifica telecamera", + "description": "Configura le impostazioni della telecamera, inclusi gli ingressi ed i ruoli della trasmissione.", + "name": "Nome della telecamera", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameInvalid": "Il nome della telecamera deve contenere solo lettere, numeri, caratteri di sottolineatura o trattini", + "namePlaceholder": "ad esempio: porta_principale", + "enabled": "Abilitata", + "ffmpeg": { + "inputs": "Flussi di ingresso", + "path": "Percorso del flusso", + "pathRequired": "Il percorso del flusso è obbligatorio", + "pathPlaceholder": "rtsp://...", + "roles": "Ruoli", + "rolesRequired": "È richiesto almeno un ruolo", + "rolesUnique": "Ogni ruolo (audio, rilevamento, registrazione) può essere assegnato solo ad un flusso", + "addInput": "Aggiungi flusso di ingresso", + "removeInput": "Rimuovi flusso di ingresso", + "inputsRequired": "È richiesto almeno un flusso di ingresso" + }, + "toast": { + "success": "La telecamera {{cameraName}} è stata salvata correttamente" + } } }, "menu": { @@ -428,7 +470,7 @@ "general": { "liveDashboard": { "automaticLiveView": { - "desc": "Passa automaticamente alla visualizzazione dal vivodi una telecamera quando viene rilevata attività. Disattivando questa opzione, le immagini statiche della telecamera nella schermata dal vivo verranno aggiornate solo una volta al minuto.", + "desc": "Passa automaticamente alla visualizzazione dal vivo di una telecamera quando viene rilevata attività. Disattivando questa opzione, le immagini statiche della telecamera nella schermata dal vivo verranno aggiornate solo una volta al minuto.", "label": "Visualizzazione automatica dal vivo" }, "playAlertVideos": { @@ -676,11 +718,106 @@ "title": "Classificazione degli uccelli" }, "licensePlateRecognition": { - "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo \"car\". Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", + "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo automobile (car). Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", "title": "Riconoscimento della targa", "readTheDocumentation": "Leggi la documentazione" }, "unsavedChanges": "Modifiche alle impostazioni di miglioramento non salvate", "restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)" + }, + "triggers": { + "documentTitle": "Inneschi", + "management": { + "title": "Gestione inneschi", + "desc": "Gestisci gli inneschi per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." + }, + "addTrigger": "Aggiungi innesco", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Contenuto", + "threshold": "Soglia", + "actions": "Azioni", + "noTriggers": "Nessun innesco configurato per questa telecamera.", + "edit": "Modifica", + "deleteTrigger": "Elimina innesco", + "lastTriggered": "Ultimo innesco" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "actions": { + "alert": "Contrassegna come avviso", + "notification": "Invia notifica" + }, + "dialog": { + "createTrigger": { + "title": "Crea innesco", + "desc": "Crea un innesco per la telecamera {{camera}}" + }, + "editTrigger": { + "title": "Modifica innesco", + "desc": "Modifica le impostazioni per l'innesco della telecamera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimina innesco", + "desc": "Vuoi davvero eliminare l'innesco {{triggerName}}? Questa azione non può essere annullata." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Inserisci il nome dell'innesco", + "error": { + "minLength": "Il nome deve essere lungo almeno 2 caratteri.", + "invalidCharacters": "Il nome può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", + "alreadyExists": "Per questa telecamera esiste già un innesco con questo nome." + } + }, + "enabled": { + "description": "Abilita o disabilita questo innesco" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleziona il tipo di innesco" + }, + "content": { + "title": "Contenuto", + "imagePlaceholder": "Seleziona un'immagine", + "textPlaceholder": "Inserisci il contenuto del testo", + "imageDesc": "Seleziona un'immagine per attivare questa azione quando viene rilevata un'immagine simile.", + "textDesc": "Inserisci il testo per attivare questa azione quando viene rilevata una descrizione simile dell'oggetto tracciato.", + "error": { + "required": "Il contenuto è obbligatorio." + } + }, + "threshold": { + "title": "Soglia", + "error": { + "min": "La soglia deve essere almeno 0", + "max": "La soglia deve essere al massimo 1" + } + }, + "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.", + "error": { + "min": "È necessario selezionare almeno un'azione." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "L'innesco {{name}} è stato creato correttamente.", + "updateTrigger": "L'innesco {{name}} è stato aggiornato correttamente.", + "deleteTrigger": "L'innesco {{name}} è stato eliminato correttamente." + }, + "error": { + "createTriggerFailed": "Impossibile creare l'innesco: {{errorMessage}}", + "updateTriggerFailed": "Impossibile aggiornare l'innesco: {{errorMessage}}", + "deleteTriggerFailed": "Impossibile eliminare l'innesco: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/it/views/system.json b/web/public/locales/it/views/system.json index ee838403c..c8d298987 100644 --- a/web/public/locales/it/views/system.json +++ b/web/public/locales/it/views/system.json @@ -83,10 +83,10 @@ }, "enrichments": { "embeddings": { - "face_embedding_speed": "Velocità incorporazione volti", + "face_embedding_speed": "Velocità incorporamento volti", "plate_recognition_speed": "Velocità riconoscimento targhe", - "image_embedding_speed": "Velocità incorporazione immagini", - "text_embedding_speed": "Velocità incorporazione testo", + "image_embedding_speed": "Velocità incorporamento immagini", + "text_embedding_speed": "Velocità incorporamento testo", "face_recognition_speed": "Velocità di riconoscimento facciale", "face_recognition": "Riconoscimento facciale", "plate_recognition": "Riconoscimento delle targhe", @@ -102,7 +102,7 @@ "info": { "fetching": "Recupero dati della telecamera", "streamDataFromFFPROBE": "I dati del flusso vengono ottenuti con ffprobe.", - "cameraProbeInfo": "Informazioni analisi telecamera {{camera}}", + "cameraProbeInfo": "Informazioni flussi telecamera {{camera}}", "stream": "Flusso {{idx}}", "video": "Video:", "codec": "Codec:", @@ -112,7 +112,7 @@ "audio": "Audio:", "error": "Errore: {{error}}", "tips": { - "title": "Informazioni analisi telecamera" + "title": "Informazioni flussi telecamera" }, "aspectRatio": "rapporto d'aspetto" }, @@ -121,15 +121,15 @@ "framesAndDetections": "Fotogrammi / Rilevamenti", "label": { "camera": "telecamera", - "detect": "rileva", - "skipped": "saltato", + "detect": "rilevamento", + "skipped": "saltati", "ffmpeg": "FFmpeg", "capture": "cattura", "overallFramesPerSecond": "fotogrammi totali al secondo", "overallDetectionsPerSecond": "rilevamenti totali al secondo", "overallSkippedDetectionsPerSecond": "rilevamenti totali saltati al secondo", "cameraCapture": "{{camName}} cattura", - "cameraDetect": "{{camName}} rileva", + "cameraDetect": "{{camName}} rilevamento", "cameraFramesPerSecond": "{{camName}} fotogrammi al secondo", "cameraDetectionsPerSecond": "{{camName}} rilevamenti al secondo", "cameraSkippedDetectionsPerSecond": "{{camName}} rilevamenti saltati al secondo", @@ -174,6 +174,11 @@ "title": "Liberi", "tips": "Questo valore potrebbe non rappresentare accuratamente lo spazio libero disponibile per Frigate se nel disco sono archiviati altri file oltre alle registrazioni di Frigate. Frigate non tiene traccia dell'utilizzo dello spazio di archiviazione al di fuori delle sue registrazioni." } + }, + "shm": { + "title": "Allocazione SHM (memoria condivisa)", + "warning": "La dimensione SHM attuale di {{total}} MB è troppo piccola. Aumentarla ad almeno {{min_shm}} MB.", + "readTheDocumentation": "Leggi la documentazione" } }, "lastRefreshed": "Ultimo aggiornamento: " diff --git a/web/public/locales/ja/common.json b/web/public/locales/ja/common.json index 44b1d2b51..bf06ac831 100644 --- a/web/public/locales/ja/common.json +++ b/web/public/locales/ja/common.json @@ -3,5 +3,6 @@ "untilForRestart": "Frigateが再起動するまで.", "untilRestart": "再起動まで", "untilForTime": "{{time}} まで" - } + }, + "readTheDocumentation": "ドキュメントを読む" } diff --git a/web/public/locales/ko/common.json b/web/public/locales/ko/common.json index 0967ef424..0baf54f2a 100644 --- a/web/public/locales/ko/common.json +++ b/web/public/locales/ko/common.json @@ -1 +1,3 @@ -{} +{ + "readTheDocumentation": "문서 읽기" +} diff --git a/web/public/locales/lt/common.json b/web/public/locales/lt/common.json index 0f512e147..936cb217a 100644 --- a/web/public/locales/lt/common.json +++ b/web/public/locales/lt/common.json @@ -47,12 +47,43 @@ "second_few": "{{time}} sekundės", "second_other": "{{time}} sekundžių", "formattedTimestamp": { - "12hour": "" + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" } }, "unit": { "speed": { - "kph": "kmh" + "kph": "kmh", + "mph": "mph" }, "length": { "feet": "pėdos", @@ -82,21 +113,22 @@ "pictureInPicture": "Paveikslėlis Paveiksle", "twoWayTalk": "Dvikryptis Kalbėjimas", "cameraAudio": "Kameros Garsas", - "on": "", + "on": "ON", "edit": "Redaguoti", "copyCoordinates": "Kopijuoti koordinates", "delete": "Ištrinti", "yes": "Taip", "no": "Ne", "download": "Atsisiųsti", - "info": "", + "info": "Info", "suspended": "Pristatbdytas", "unsuspended": "Atnaujinti", "play": "Groti", "unselect": "Atžymėti", "export": "Eksportuoti", "deleteNow": "Trinti Dabar", - "next": "Kitas" + "next": "Kitas", + "off": "OFF" }, "menu": { "system": "Sistema", @@ -131,10 +163,24 @@ "hu": "Vengrų", "fi": "Suomių", "da": "Danų", - "sk": "Slovėnų", + "sk": "Slovakų", "withSystem": { "label": "Kalbai naudoti sistemos nustatymus" - } + }, + "hi": "Hindi", + "ptBR": "Brazilietiška Portugalų", + "ko": "Korėjiečių", + "he": "Hebrajų", + "yue": "Kantoniečių", + "th": "Tailandiečių", + "ca": "Kataloniečių", + "sr": "Serbų", + "sl": "Slovėnų", + "lt": "Lietuvių", + "bg": "Bulgarų", + "gl": "Galician", + "id": "Indonesian", + "ur": "Urdu" }, "appearance": "Išvaizda", "darkMode": { @@ -182,7 +228,8 @@ "anonymous": "neidentifikuotas", "logout": "atsijungti", "setPassword": "Nustatyti Slaptažodi" - } + }, + "uiPlayground": "UI Playground" }, "toast": { "copyUrlToClipboard": "URL nukopijuotas į atmintį.", @@ -209,6 +256,19 @@ "next": { "title": "Sekantis", "label": "Eiti į sekantį puslapį" - } - } + }, + "more": "Daugiau puslapių" + }, + "accessDenied": { + "documentTitle": "Priegai Nesuteikta - Frigate", + "title": "Prieiga Nesuteikta", + "desc": "Jūs neturite leidimo žiūrėti šį puslapį." + }, + "notFound": { + "documentTitle": "Nerasta - Frigate", + "title": "404", + "desc": "Puslapis nerastas" + }, + "selectItem": "Pasirinkti {{item}}", + "readTheDocumentation": "Skaityti dokumentaciją" } diff --git a/web/public/locales/lt/components/camera.json b/web/public/locales/lt/components/camera.json index 11639ade2..9341771f7 100644 --- a/web/public/locales/lt/components/camera.json +++ b/web/public/locales/lt/components/camera.json @@ -15,8 +15,72 @@ "placeholder": "Įveskite pavadinimą…", "errorMessage": { "mustLeastCharacters": "Kamerų grupės pavadinimas turi būti bent 2 simbolių.", - "exists": "Kamerų grupės pavadinimas jau egzistuoja." + "exists": "Kamerų grupės pavadinimas jau egzistuoja.", + "nameMustNotPeriod": "Kamerų grupės pavadinime negali būti taško.", + "invalid": "Nepriimtinas kamera grupės pavadinimas." + } + }, + "cameras": { + "label": "Kameros", + "desc": "Pasirinkite kameras šiai grupei." + }, + "icon": "Ikona", + "success": "Kameraų grupė {{name}} išsaugota.", + "camera": { + "setting": { + "label": "Kamerų Transliacijos Nustatymai", + "title": "{{cameraName}} Transliavimo Nustatymai", + "desc": "Keisti tiesioginės tranliacijos nustatymus šiai kamerų grupės valdymo lentai. Šie nustatymai yra specifiniai įrenginiui/ naršyklei.", + "audioIsAvailable": "Šiai transliacijai yra garso takelis", + "audioIsUnavailable": "Šiai transliacijai nėra garso takelio", + "audio": { + "tips": { + "title": "Šiai transliacijai garsas turi būti teikiamas iš kameros ir konfiguruojamas naudojant go2rtc.", + "document": "Skaityti dokumentaciją " + } + }, + "stream": "Transliacija", + "placeholder": "Pasirinkti transliaciją", + "streamMethod": { + "label": "Transliacijos Metodas", + "placeholder": "Pasirinkti transliacijos metodą", + "method": { + "noStreaming": { + "label": "Nėra transliacijos", + "desc": "Kameros vaizdas atsinaujins tik kartą per mintuę ir nebus tiesioginės transliacijos." + }, + "smartStreaming": { + "label": "Išmanus Transliavimas (rekomenduojama)", + "desc": "Išmanus transliavimas atnaujins jūsų kameros vaizdą kartą per minutę jei nebus aptinkama jokia veikla tam kad saugoti tinklo pralaiduma ir kitus resursus. Aptikus veiklą atvaizdavimas nepertraukiamai persijungs į tiesioginę transliaciją." + }, + "continuousStreaming": { + "label": "Nuolatinė Transliacija", + "desc": { + "title": "Kameros vaizdas visada bus tiesioginė transliacija, jei jis bus matomas valdymo lentoje, net jei jokia veikla nėra aptinkama.", + "warning": "Nepertraukiama transliacija gali naudoti daug tinklo duomenų bei sukelti našumo problemų. Naudoti su atsarga." + } + } + } + }, + "compatibilityMode": { + "desc": "Šį nustatymą naudoti tik jei jūsų kameros tiesioginėje transliacijoje matomi spalvų neatitikimai arba matoma įstriža linija dešinėje vaizdo pusėje.", + "label": "Suderinamumo rėžimas" + } } } + }, + "debug": { + "options": { + "label": "Nustatymai", + "title": "Pasirinkimai", + "showOptions": "Rodyti Pasirinkimus", + "hideOptions": "Slėpti Pasirinkimus" + }, + "boundingBox": "Ribojantis Kvadratas", + "timestamp": "Laiko žymė", + "zones": "Zonos", + "mask": "Maskuotė", + "motion": "Judesys", + "regions": "Regionas" } } diff --git a/web/public/locales/lt/components/dialog.json b/web/public/locales/lt/components/dialog.json index 4feb8d583..31c39611a 100644 --- a/web/public/locales/lt/components/dialog.json +++ b/web/public/locales/lt/components/dialog.json @@ -21,5 +21,10 @@ "label": "Pateiktį į Frigate+" } } + }, + "streaming": { + "restreaming": { + "disabled": "Šiai kamerai pertransliavimas nėra įjungtas" + } } } diff --git a/web/public/locales/lt/objects.json b/web/public/locales/lt/objects.json index bc7499b61..9aa9b5d70 100644 --- a/web/public/locales/lt/objects.json +++ b/web/public/locales/lt/objects.json @@ -105,14 +105,16 @@ "license_plate": "Registracijos Numeris", "package": "Pakuotė", "bbq_grill": "BBQ kepsninė", - "amazon": "", - "usps": "", - "ups": "", - "fedex": "", - "dhl": "", - "an_post": "", - "purolator": "", - "postnl": "", - "nzpost": "", - "postnord": "" + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" } diff --git a/web/public/locales/lt/views/faceLibrary.json b/web/public/locales/lt/views/faceLibrary.json index d4dce21f3..1abec11bd 100644 --- a/web/public/locales/lt/views/faceLibrary.json +++ b/web/public/locales/lt/views/faceLibrary.json @@ -9,5 +9,6 @@ "face": "Veido detelės", "timestamp": "Laiko žyma", "unknown": "Nežinoma" - } + }, + "selectItem": "Pasirinkti {{item}}" } diff --git a/web/public/locales/lt/views/search.json b/web/public/locales/lt/views/search.json index d970b3d2d..151e7820a 100644 --- a/web/public/locales/lt/views/search.json +++ b/web/public/locales/lt/views/search.json @@ -12,7 +12,61 @@ "trackedObjectId": "Sekamo Objekto ID", "filter": { "label": { - "cameras": "Kameros" + "cameras": "Kameros", + "labels": "Etiketės", + "zones": "Zonos", + "search_type": "Paieškos Tipas", + "time_range": "Laiko rėžis", + "before": "Prieš", + "after": "Po", + "min_score": "Min Balas", + "max_score": "Max Balas", + "min_speed": "Min Greitis", + "max_speed": "Max Greitis", + "recognized_license_plate": "Atpažinti Registravimo Numeriai", + "has_clip": "Turi Klipą", + "has_snapshot": "Turi Nuotrauką", + "sub_labels": "Sub Etiketės" + }, + "searchType": { + "thumbnail": "Miniatiūra", + "description": "Aprašymas" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data 'prieš' turi būti vėliau nei data 'po'.", + "afterDatebeEarlierBefore": "Data 'po' turi būti anksčiau nei data 'prieš'.", + "minScoreMustBeLessOrEqualMaxScore": "'min balas' turi būti mažesnis arba lygus 'max balui'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max balas' turi būti didesnis arba lygus 'min balui'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min greitis' privalo būti mažesnis arba lygus 'max greičiui'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max greitis' privalo būti didesnis arba lygus 'min greičiui'." + } + }, + "tips": { + "title": "Kaip naudoti tekstinius filtrus", + "desc": { + "text": "Filtrai leidžia susiaurinti paieškos rezultatus. Štai kaip juos naudoti įvesties laukelyje:", + "step1": "Type a filter key name followed by a colon (e.g., \"cameras:\").", + "step2": "Pasirinkite reikšmę iš siūlomų arba įveskite savo sugalvotą.", + "step3": "Naudokite kelis filtrus įvesdami juos vieną paskui kitą su tarpu tarp jų.", + "step5": "Laiko rėžio filtro naudojamas {{exampleTime}} formatas.", + "step6": "Pašalinti filtrus spaudžiant 'x' šalia jų.", + "exampleLabel": "Pavyzdys:", + "step4": "Date filters (before: and after:) use {{DateFormat}} format." + } + }, + "header": { + "currentFilterType": "Filtruoti Reikšmes", + "noFilters": "Filtrai", + "activeFilters": "Aktyvūs Filtrai" } + }, + "similaritySearch": { + "title": "Panašumų Paieška", + "active": "Panašumų paieška aktyvi", + "clear": "Išvalyti panašumų paiešką" + }, + "placeholder": { + "search": "Ieškoma…" } } diff --git a/web/public/locales/lt/views/system.json b/web/public/locales/lt/views/system.json index fb9784cf7..7e140b08f 100644 --- a/web/public/locales/lt/views/system.json +++ b/web/public/locales/lt/views/system.json @@ -7,13 +7,104 @@ "go2rtc": "Go2RTC Žurnalas - Frigate", "nginx": "Nginx Žurnalas - Frigate" }, - "general": "Bendroji Statistika - Frigate" + "general": "Bendroji Statistika - Frigate", + "enrichments": "Pagerinimų Statistika - Frigate" }, "title": "Sistema", "metrics": "Sistemos metrikos", "logs": { "download": { "label": "Parsisiųsti Žurnalą" + }, + "copy": { + "label": "Kopijuoti į iškarpinę", + "success": "Nukopijuoti įrašai į iškarpinę", + "error": "Nepavyko nukopijuoti įrašų į iškarpinę" + }, + "type": { + "label": "Tipas", + "timestamp": "Laiko žymė", + "tag": "Žyma", + "message": "Žinutė" + }, + "tips": "Įrašai yra transliuojami iš serverio", + "toast": { + "error": { + "fetchingLogsFailed": "Klaida nuskaitant įrašus: {{errorMessage}}", + "whileStreamingLogs": "Klaidai transliuojant įrašus: {{errorMessage}}" + } } + }, + "general": { + "title": "Bendrinis", + "detector": { + "title": "Detektoriai", + "inferenceSpeed": "Detektorių darbo greitis", + "temperature": "Detektorių Temperatūra", + "cpuUsage": "Detektorių CPU Naudojimas", + "memoryUsage": "Detektorių Atminties Naudojimas" + }, + "hardwareInfo": { + "title": "Techninės įrangos Info", + "gpuUsage": "GPU Naudojimas", + "gpuMemory": "GPU Atmintis", + "gpuEncoder": "GPU Kodavimas", + "gpuDecoder": "GPU Dekodavimas", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Išvestis", + "returnCode": "Grįžtamas Kodas: {{code}}", + "processOutput": "Proceso Išvestis:", + "processError": "Proceso Klaida:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI išvestis:", + "name": "Pavadinimas: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA Compute Galimybės: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Užverti GPU info" + }, + "copyInfo": { + "label": "Kopijuoti GPU Info" + }, + "toast": { + "success": "Nukopijuota GPU info į iškarpinę" + } + }, + "npuUsage": "NPU Naudojimas", + "npuMemory": "NPU Atmintis" + }, + "otherProcesses": { + "title": "Kiti Procesai", + "processCpuUsage": "Procesų CPU Naudojimas", + "processMemoryUsage": "Procesu Atminties Naudojimas" + } + }, + "storage": { + "title": "Saugykla", + "overview": "Apžvalga", + "recordings": { + "title": "Įrašai", + "tips": "Ši reikšmė nurodo kiek iš viso Frigate duombazėje esantys įrašai užima vietos saugykloje. Frigate neseka kiek vietos užima visi kiti failai esantys laikmenoje.", + "earliestRecording": "Anksčiausias esantis įrašas:" + }, + "cameraStorage": { + "title": "Kameros Saugykla", + "camera": "Kamera", + "unusedStorageInformation": "Neišnaudotos Saugyklos Informacija", + "storageUsed": "Saugykla", + "percentageOfTotalUsed": "Procentas nuo Viso", + "bandwidth": "Pralaidumas", + "unused": { + "title": "Nepanaudota", + "tips": "Jei saugykloje turite daugiau failų apart Frigate įrašų, ši reikšmė neatspindės tikslios likusios laisvos vietos Frigate panaudojimui. Frigate neseka saugyklos panaudojimo už savo įrašų ribų." + } + } + }, + "cameras": { + "title": "Kameros" } } diff --git a/web/public/locales/nb-NO/common.json b/web/public/locales/nb-NO/common.json index df446387f..af3d8b7b7 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -190,7 +190,15 @@ "uk": "Українська (Ukrainsk)", "yue": "粵語 (Kantonesisk)", "th": "ไทย (Thai)", - "ca": "Català (Katalansk)" + "ca": "Català (Katalansk)", + "ptBR": "Português brasileiro (Brasiliansk portugisisk)", + "sr": "Српски (Serbisk)", + "sl": "Slovenščina (Slovensk)", + "lt": "Lietuvių (Litauisk)", + "bg": "Български (Bulgarsk)", + "gl": "Galego (Galisisk)", + "id": "Bahasa Indonesia (Indonesisk)", + "ur": "اردو (Urdu)" }, "appearance": "Utseende", "darkMode": { @@ -264,5 +272,6 @@ "title": "404", "desc": "Siden ble ikke funnet" }, - "selectItem": "Velg {{item}}" + "selectItem": "Velg {{item}}", + "readTheDocumentation": "Se dokumentasjonen" } diff --git a/web/public/locales/nb-NO/components/dialog.json b/web/public/locales/nb-NO/components/dialog.json index 93a65c99d..58ff2e3f9 100644 --- a/web/public/locales/nb-NO/components/dialog.json +++ b/web/public/locales/nb-NO/components/dialog.json @@ -119,5 +119,12 @@ "markAsReviewed": "Merk som inspisert", "deleteNow": "Slett nå" } + }, + "imagePicker": { + "selectImage": "Velg et sporet objekts miniatyrbilde", + "search": { + "placeholder": "Søk etter (under-)merkelapp..." + }, + "noImages": "Ingen miniatyrbilder funnet for dette kameraet" } } diff --git a/web/public/locales/nb-NO/components/filter.json b/web/public/locales/nb-NO/components/filter.json index 5bcbf5d08..979a634d1 100644 --- a/web/public/locales/nb-NO/components/filter.json +++ b/web/public/locales/nb-NO/components/filter.json @@ -123,5 +123,13 @@ "title": "Alle soner", "short": "Soner" } + }, + "classes": { + "label": "Klasser", + "all": { + "title": "Alle klasser" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klasser" } } diff --git a/web/public/locales/nb-NO/views/configEditor.json b/web/public/locales/nb-NO/views/configEditor.json index 09f0b1c69..c0c9253fa 100644 --- a/web/public/locales/nb-NO/views/configEditor.json +++ b/web/public/locales/nb-NO/views/configEditor.json @@ -12,5 +12,7 @@ "copyConfig": "Kopier konfigurasjonen", "saveAndRestart": "Lagre og omstart", "saveOnly": "Kun lagre", - "confirm": "Avslutt uten å lagre?" + "confirm": "Avslutt uten å lagre?", + "safeConfigEditor": "Konfigurasjonsredigering (Sikker modus)", + "safeModeDescription": "Frigate er i sikker modus grunnet en feil i validering av konfigurasjonen." } diff --git a/web/public/locales/nb-NO/views/events.json b/web/public/locales/nb-NO/views/events.json index 70d24e20e..e333fb5ef 100644 --- a/web/public/locales/nb-NO/views/events.json +++ b/web/public/locales/nb-NO/views/events.json @@ -34,5 +34,7 @@ "markTheseItemsAsReviewed": "Merk disse elementene som inspiserte", "selected_one": "{{count}} valgt", "selected_other": "{{count}} valgt", - "detected": "detektert" + "detected": "detektert", + "suspiciousActivity": "Mistenkelig aktivitet", + "threateningActivity": "Truende aktivitet" } diff --git a/web/public/locales/nb-NO/views/explore.json b/web/public/locales/nb-NO/views/explore.json index e95dbfda2..30e14e8de 100644 --- a/web/public/locales/nb-NO/views/explore.json +++ b/web/public/locales/nb-NO/views/explore.json @@ -89,12 +89,14 @@ "success": { "updatedSublabel": "Under-merkelapp oppdatert med suksess.", "updatedLPR": "Vellykket oppdatering av kjennemerke.", - "regenerate": "En ny beskrivelse har blitt anmodet fra {{provider}}. Avhengig av hastigheten til leverandøren din, kan den nye beskrivelsen ta litt tid å regenerere." + "regenerate": "En ny beskrivelse har blitt anmodet fra {{provider}}. Avhengig av hastigheten til leverandøren din, kan den nye beskrivelsen ta litt tid å regenerere.", + "audioTranscription": "Lydtranskripsjon ble forespurt." }, "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 under-merkelapp: {{errorMessage}}", + "audioTranscription": "Forespørsel om lydtranskripsjon feilet: {{errorMessage}}" } }, "desc": "Detaljer for inspeksjonselement", @@ -146,6 +148,9 @@ }, "snapshotScore": { "label": "Øyeblikksbilde poengsum" + }, + "score": { + "label": "Poengsum" } }, "itemMenu": { @@ -175,6 +180,14 @@ "submitToPlus": { "label": "Send til Frigate+", "aria": "Send til Frigate Plus" + }, + "addTrigger": { + "label": "Legg til utløser", + "aria": "Legg til en utløser for dette sporede objektet" + }, + "audioTranscription": { + "label": "Transkriber", + "aria": "Forespør lydtranskripsjon" } }, "searchResult": { @@ -203,5 +216,8 @@ "fetchingTrackedObjectsFailed": "Feil ved henting av sporede objekter: {{errorMessage}}", "trackedObjectsCount_one": "{{count}} sporet objekt ", "trackedObjectsCount_other": "{{count}} sporede objekter ", - "exploreMore": "Utforsk flere {{label}} objekter" + "exploreMore": "Utforsk flere {{label}} objekter", + "aiAnalysis": { + "title": "AI-Analyse" + } } diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json index 2183cebb9..70d008657 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -35,6 +35,14 @@ "center": { "label": "Klikk i rammen for å sentrere PTZ-kameraet" } + }, + "focus": { + "in": { + "label": "Fokuser inn på PTZ kamera" + }, + "out": { + "label": "Fokuser ut på PTZ kamera" + } } }, "camera": { @@ -153,6 +161,11 @@ "recording": "Opptak", "snapshots": "Øyeblikksbilder", "audioDetection": "Lydregistrering", - "autotracking": "Automatisk sporing" + "autotracking": "Automatisk sporing", + "transcription": "Lydtranskripsjon" + }, + "transcription": { + "enable": "Aktiver direkte lydtranskripsjon", + "disable": "Deaktiver direkte lydtranskripsjon" } } diff --git a/web/public/locales/nl/common.json b/web/public/locales/nl/common.json index 0af38d8a5..166dee4b5 100644 --- a/web/public/locales/nl/common.json +++ b/web/public/locales/nl/common.json @@ -175,7 +175,15 @@ "ja": "日本語 (Japans)", "yue": "粵語 (Kantonees)", "th": "ไทย (Thais)", - "ca": "Català (Catalaans)" + "ca": "Català (Catalaans)", + "ptBR": "Português brasileiro (Braziliaans Portugees)", + "sr": "Српски (Servisch)", + "sl": "Slovenščina (Sloveens)", + "lt": "Lietuvių (Litouws)", + "bg": "Български (Bulgaars)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)", + "ur": "اردو (Urdu)" }, "darkMode": { "label": "Donkere modus", @@ -264,5 +272,6 @@ "title": "404", "documentTitle": "Niet gevonden - Frigate" }, - "selectItem": "Selecteer {{item}}" + "selectItem": "Selecteer {{item}}", + "readTheDocumentation": "Lees de documentatie" } diff --git a/web/public/locales/nl/components/dialog.json b/web/public/locales/nl/components/dialog.json index 0c1e8aaf3..65ee012fd 100644 --- a/web/public/locales/nl/components/dialog.json +++ b/web/public/locales/nl/components/dialog.json @@ -119,5 +119,12 @@ "success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd." } } + }, + "imagePicker": { + "selectImage": "Kies thumbnail van gevolgd object", + "noImages": "Geen thumbnails gevonden voor deze camera", + "search": { + "placeholder": "Zoeken op label of sub label..." + } } } diff --git a/web/public/locales/nl/components/filter.json b/web/public/locales/nl/components/filter.json index fa2ecd9d0..14716ecb2 100644 --- a/web/public/locales/nl/components/filter.json +++ b/web/public/locales/nl/components/filter.json @@ -123,5 +123,13 @@ "label": "Filters resetten naar standaardwaarden" }, "more": "Meer filters", - "estimatedSpeed": "Geschatte snelheid ({{unit}})" + "estimatedSpeed": "Geschatte snelheid ({{unit}})", + "classes": { + "label": "Klassen", + "all": { + "title": "Alle klassen" + }, + "count_one": "{{count}} klasse", + "count_other": "{{count}} Klassen" + } } diff --git a/web/public/locales/nl/views/configEditor.json b/web/public/locales/nl/views/configEditor.json index 5bd94a242..50a146cb6 100644 --- a/web/public/locales/nl/views/configEditor.json +++ b/web/public/locales/nl/views/configEditor.json @@ -12,5 +12,7 @@ }, "configEditor": "Configuratie Bewerken", "saveOnly": "Alleen opslaan", - "confirm": "Afsluiten zonder op te slaan?" + "confirm": "Afsluiten zonder op te slaan?", + "safeConfigEditor": "Configuratie-editor (veilige modus)", + "safeModeDescription": "Frigate is in veilige modus vanwege een configuratievalidatiefout." } diff --git a/web/public/locales/nl/views/events.json b/web/public/locales/nl/views/events.json index 269cadffc..3f42b6c29 100644 --- a/web/public/locales/nl/views/events.json +++ b/web/public/locales/nl/views/events.json @@ -34,5 +34,7 @@ "markTheseItemsAsReviewed": "Markeer deze items als beoordeeld", "selected_other": "{{count}} geselecteerd", "selected_one": "{{count}} geselecteerd", - "detected": "gedetecteerd" + "detected": "gedetecteerd", + "suspiciousActivity": "Verdachte activiteit", + "threateningActivity": "Bedreigende activiteit" } diff --git a/web/public/locales/nl/views/explore.json b/web/public/locales/nl/views/explore.json index 78c2c7116..a7a8f41e4 100644 --- a/web/public/locales/nl/views/explore.json +++ b/web/public/locales/nl/views/explore.json @@ -102,12 +102,14 @@ "success": { "regenerate": "Er is een nieuwe beschrijving aangevraagd bij {{provider}}. Afhankelijk van de snelheid van je provider kan het regenereren van de nieuwe beschrijving enige tijd duren.", "updatedSublabel": "Sublabel succesvol bijgewerkt.", - "updatedLPR": "Kenteken succesvol bijgewerkt." + "updatedLPR": "Kenteken succesvol bijgewerkt.", + "audioTranscription": "Audiotranscriptie succesvol aangevraagd." }, "error": { "updatedSublabelFailed": "Het is niet gelukt om het sublabel bij te werken: {{errorMessage}}", "regenerate": "Het is niet gelukt om {{provider}} aan te roepen voor een nieuwe beschrijving: {{errorMessage}}", - "updatedLPRFailed": "Kentekenplaat bijwerken mislukt: {{errorMessage}}" + "updatedLPRFailed": "Kentekenplaat bijwerken mislukt: {{errorMessage}}", + "audioTranscription": "Audiotranscriptie aanvragen mislukt: {{errorMessage}}" } } }, @@ -153,6 +155,9 @@ "recognizedLicensePlate": "Erkende kentekenplaat", "snapshotScore": { "label": "Snapshot scoren" + }, + "score": { + "label": "Score" } }, "itemMenu": { @@ -182,6 +187,14 @@ "downloadSnapshot": { "label": "Download snapshot", "aria": "Download snapshot" + }, + "addTrigger": { + "label": "Trigger toevoegen", + "aria": "Voeg een trigger toe voor dit gevolgde object" + }, + "audioTranscription": { + "label": "Transcriberen", + "aria": "Audiotranscriptie aanvragen" } }, "noTrackedObjects": "Geen gevolgde objecten gevonden", @@ -203,5 +216,11 @@ } }, "fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}", - "exploreMore": "Verken meer {{label}} objecten" + "exploreMore": "Verken meer {{label}} objecten", + "aiAnalysis": { + "title": "AI-analyse" + }, + "concerns": { + "label": "Zorgen" + } } diff --git a/web/public/locales/nl/views/live.json b/web/public/locales/nl/views/live.json index d09f4c699..00bb28a92 100644 --- a/web/public/locales/nl/views/live.json +++ b/web/public/locales/nl/views/live.json @@ -41,7 +41,15 @@ "label": "Klik in het frame om de PTZ-camera te centreren" } }, - "presets": "PTZ-camerapresets" + "presets": "PTZ-camerapresets", + "focus": { + "in": { + "label": "Focus PTZ-camera in" + }, + "out": { + "label": "Focus PTZ-camera uit" + } + } }, "camera": { "enable": "Camera inschakelen", @@ -135,7 +143,8 @@ "audioDetection": "Audiodetectie", "autotracking": "Automatisch volgen", "snapshots": "Momentopnames", - "cameraEnabled": "Camera ingeschakeld" + "cameraEnabled": "Camera ingeschakeld", + "transcription": "Audiotranscriptie" }, "history": { "label": "Historische beelden weergeven" @@ -154,5 +163,9 @@ "group": { "label": "Cameragroep bewerken" } + }, + "transcription": { + "enable": "Live audiotranscriptie inschakelen", + "disable": "Live audiotranscriptie uitschakelen" } } diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json index 5189f57bf..4a7dc2781 100644 --- a/web/public/locales/nl/views/settings.json +++ b/web/public/locales/nl/views/settings.json @@ -178,7 +178,44 @@ "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
Let op: dit schakelt go2rtc-restreams niet uit.", "title": "Streams" }, - "title": "Camera-instellingen" + "title": "Camera-instellingen", + "object_descriptions": { + "title": "AI-gegenereerde objectomschrijvingen", + "desc": "AI-gegenereerde objectomschrijvingen tijdelijk uitschakelen voor deze camera. Wanneer uitgeschakeld, zullen omschrijvingen van gevolgde objecten op deze camera niet aangevraagd worden." + }, + "review_descriptions": { + "title": "Generatieve-AI Beoordelingsbeschrijvingen", + "desc": "Tijdelijk generatieve-AI-beoordelingsbeschrijvingen voor deze camera in- of uitschakelen. Wanneer dit is uitgeschakeld, worden er geen door AI gegenereerde beschrijvingen opgevraagd voor beoordelingsitems van deze camera." + }, + "addCamera": "Nieuwe camera toevoegen", + "editCamera": "Camera bewerken:", + "selectCamera": "Selecteer een camera", + "backToSettings": "Terug naar camera-instellingen", + "cameraConfig": { + "add": "Camera toevoegen", + "edit": "Camera bewerken", + "description": "Configureer de camera-instellingen, inclusief streaming-inputs en functies.", + "name": "Cameranaam", + "nameRequired": "Cameranaam is vereist", + "nameInvalid": "De cameranaam mag alleen letters, cijfers, onderstrepingstekens of koppeltekens bevatten", + "namePlaceholder": "bijv. voor_deur", + "enabled": "Ingeschakeld", + "ffmpeg": { + "inputs": "Streams-Input", + "path": "Stroompad", + "pathRequired": "Streampad is vereist", + "pathPlaceholder": "rtsp://...", + "roles": "Functie", + "rolesRequired": "Er is ten minste één functie vereist", + "rolesUnique": "Elke functie (audio, detecteren, opnemen) kan slechts aan één stream worden toegewezen", + "addInput": "Inputstream toevoegen", + "removeInput": "Inputstream verwijderen", + "inputsRequired": "Er is ten minste één stream-input vereist" + }, + "toast": { + "success": "Camera {{cameraName}} is succesvol opgeslagen" + } + } }, "masksAndZones": { "filter": { @@ -420,7 +457,12 @@ "score": "Score", "ratio": "Verhouding" }, - "detectorDesc": "Frigate gebruikt je detectoren ({{detectors}}) om objecten in de videostream van je camera te detecteren." + "detectorDesc": "Frigate gebruikt je detectoren ({{detectors}}) om objecten in de videostream van je camera te detecteren.", + "paths": { + "title": "Paden", + "desc": "Toon belangrijke punten van het pad van het gevolgde object", + "tips": "

Paden


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

" + } }, "users": { "title": "Gebruikers", @@ -680,5 +722,100 @@ "success": "Verrijkingsinstellingen zijn opgeslagen. Start Frigate opnieuw op om je wijzigingen toe te passen.", "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" } + }, + "triggers": { + "documentTitle": "Triggers", + "management": { + "title": "Triggerbeheer", + "desc": "Beheer triggers voor {{camera}}. Gebruik het thumbnail om triggers te activeren op basis van thumbnail die lijken op het door u geselecteerde gevolgde object, en het beschrijvingstype om triggers te activeren op basis van beschrijvingen die lijken op de door u opgegeven tekst." + }, + "addTrigger": "Trigger toevoegen", + "table": { + "name": "Naam", + "type": "Type", + "content": "Inhoud", + "threshold": "Drempel", + "actions": "Acties", + "noTriggers": "Er zijn geen triggers geconfigureerd voor deze camera.", + "edit": "Bewerken", + "deleteTrigger": "Trigger verwijderen", + "lastTriggered": "Laatst geactiveerd" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Beschrijving" + }, + "actions": { + "alert": "Markeren als waarschuwing", + "notification": "Melding verzenden" + }, + "dialog": { + "createTrigger": { + "title": "Trigger aanmaken", + "desc": "Maak een trigger voor camera {{camera}}" + }, + "editTrigger": { + "title": "Trigger bewerken", + "desc": "Wijzig de instellingen voor de trigger op camera {{camera}}" + }, + "deleteTrigger": { + "title": "Trigger verwijderen", + "desc": "Weet u zeker dat u de trigger {{triggerName}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "form": { + "name": { + "title": "Naam", + "placeholder": "Voer de naam van de trigger in", + "error": { + "minLength": "De naam moet minimaal 2 tekens lang zijn.", + "invalidCharacters": "De naam mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.", + "alreadyExists": "Er bestaat al een trigger met deze naam voor deze camera." + } + }, + "enabled": { + "description": "Deze trigger in- of uitschakelen" + }, + "type": { + "title": "Type", + "placeholder": "Selecteer het type trigger" + }, + "content": { + "title": "Inhoud", + "imagePlaceholder": "Selecteer een afbeelding", + "textPlaceholder": "Tekst invoeren", + "imageDesc": "Selecteer een afbeelding om deze actie te activeren wanneer een vergelijkbare afbeelding wordt gedetecteerd.", + "textDesc": "Voer tekst in om deze actie te activeren wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd.", + "error": { + "required": "Inhoud is vereist." + } + }, + "threshold": { + "title": "Drempel", + "error": { + "min": "De drempelwaarde moet minimaal 0 zijn", + "max": "De drempelwaarde mag maximaal 1 zijn" + } + }, + "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.", + "error": { + "min": "Er moet ten minste één actie worden geselecteerd." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} is succesvol aangemaakt.", + "updateTrigger": "Trigger {{name}} is succesvol bijgewerkt.", + "deleteTrigger": "Trigger {{name}} succesvol verwijderd." + }, + "error": { + "createTriggerFailed": "Trigger kan niet worden gemaakt: {{errorMessage}}", + "updateTriggerFailed": "Trigger kan niet worden bijgewerkt: {{errorMessage}}", + "deleteTriggerFailed": "Trigger kan niet worden verwijderd: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/nl/views/system.json b/web/public/locales/nl/views/system.json index 7d039d08e..05d90d54a 100644 --- a/web/public/locales/nl/views/system.json +++ b/web/public/locales/nl/views/system.json @@ -11,7 +11,7 @@ "enrichments": "Verrijkings Statistieken - Frigate" }, "title": "Systeem", - "metrics": "Systeem statistieken", + "metrics": "Systeemstatistieken", "logs": { "download": { "label": "Logs Downloaden" @@ -44,7 +44,7 @@ "temperature": "Detectortemperatuur" }, "hardwareInfo": { - "title": "Systeem Gegevens", + "title": "Systeemgegevens", "gpuUsage": "GPU-verbruik", "gpuInfo": { "vainfoOutput": { @@ -102,7 +102,12 @@ "camera": "Camera", "bandwidth": "Bandbreedte" }, - "title": "Opslag" + "title": "Opslag", + "shm": { + "title": "SHM (gedeeld geheugen) toewijzing", + "warning": "De huidige SHM-grootte van {{total}} MB is te klein. Vergroot deze tot minimaal {{min_shm}} MB.", + "readTheDocumentation": "Lees de documentatie" + } }, "cameras": { "title": "Cameras", @@ -154,7 +159,7 @@ "stats": { "ffmpegHighCpuUsage": "{{camera}} zorgt voor hoge FFmpeg CPU belasting ({{ffmpegAvg}}%)", "detectHighCpuUsage": "{{camera}} zorgt voor hoge detectie CPU belasting ({{detectAvg}}%)", - "healthy": "Systeem is gezond", + "healthy": "Geen problemen", "reindexingEmbeddings": "Herindexering van inbeddingen ({{processed}}% compleet)", "detectIsSlow": "{{detect}} is traag ({{speed}} ms)", "detectIsVerySlow": "{{detect}} is erg traag ({{speed}} ms)", diff --git a/web/public/locales/pl/common.json b/web/public/locales/pl/common.json index 00f14d246..058c74733 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -179,7 +179,15 @@ "fi": "Suomi (Fiński)", "yue": "粵語 (Kantoński)", "th": "ไทย (Tajski)", - "ca": "Català (Kataloński)" + "ca": "Català (Kataloński)", + "ptBR": "Português brasileiro (portugalski - Brazylia)", + "sr": "Српски (Serbski)", + "sl": "Slovenščina (Słowacki)", + "lt": "Lietuvių (Litewski)", + "bg": "Български (Bułgarski)", + "gl": "Galego (Galicyjski)", + "id": "Bahasa Indonesia (Indonezyjski)", + "ur": "اردو (Urdu)" }, "appearance": "Wygląd", "darkMode": { @@ -271,5 +279,6 @@ }, "title": "Zapisz" } - } + }, + "readTheDocumentation": "Przeczytaj dokumentację" } diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json index fd45430f3..9661160a7 100644 --- a/web/public/locales/pl/views/settings.json +++ b/web/public/locales/pl/views/settings.json @@ -640,7 +640,7 @@ } } }, - "title": "Ustawienia wzbogacania", + "title": "Ustawienia wzbogacające", "unsavedChanges": "Niezapisane zmiany ustawień wzbogacania", "birdClassification": { "title": "Klasyfikacja ptaków", diff --git a/web/public/locales/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json index 04ee37d6b..74140c039 100644 --- a/web/public/locales/pt-BR/audio.json +++ b/web/public/locales/pt-BR/audio.json @@ -1,7 +1,7 @@ { "mantra": "Mantra", "child_singing": "Criança cantando", - "speech": "Discurso", + "speech": "Fala", "yell": "Gritar", "chant": "Canto", "babbling": "Balbuciando", diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json index c5b789ccc..d187f82fa 100644 --- a/web/public/locales/pt-BR/common.json +++ b/web/public/locales/pt-BR/common.json @@ -169,7 +169,15 @@ "ca": "Català (Catalão)", "withSystem": { "label": "Usar as configurações de sistema para o idioma" - } + }, + "ptBR": "Português Brasileiro (Português Brasileiro)", + "sr": "Српски (Sérvio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Galego)", + "id": "Bahasa Indonesia (Indonésio)", + "ur": "اردو (Urdu)" }, "systemLogs": "Logs de sistema", "settings": "Configurações", @@ -210,7 +218,7 @@ "count_other": "{{count}} Câmeras" } }, - "review": "Revisão", + "review": "Revisar", "explore": "Explorar", "export": "Exportar", "uiPlayground": "Playground da UI", @@ -261,5 +269,7 @@ "documentTitle": "Não Encontrado - Frigate", "title": "404", "desc": "Página não encontrada" - } + }, + "selectItem": "Selecionar {{item}}", + "readTheDocumentation": "Leia a documentação" } diff --git a/web/public/locales/pt-BR/components/dialog.json b/web/public/locales/pt-BR/components/dialog.json index f180fe513..2e0670622 100644 --- a/web/public/locales/pt-BR/components/dialog.json +++ b/web/public/locales/pt-BR/components/dialog.json @@ -110,5 +110,12 @@ "export": "Exportar", "deleteNow": "Deletar Agora" } + }, + "imagePicker": { + "selectImage": "Selecionar a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por rótulo ou sub-rótulo…" + }, + "noImages": "Nenhuma miniatura encontrada para essa câmera" } } diff --git a/web/public/locales/pt-BR/components/filter.json b/web/public/locales/pt-BR/components/filter.json index d503d3f13..4e268be09 100644 --- a/web/public/locales/pt-BR/components/filter.json +++ b/web/public/locales/pt-BR/components/filter.json @@ -106,7 +106,7 @@ }, "trackedObjectDelete": { "title": "Confirmar Exclusão", - "desc": "Deletar esses {{objectLength}} objetos rastreados remove as capturas de imagem, qualquer embeddings salvos, e quaisquer entradas do ciclo de vida associadas do objeto. Gravações desses objetos rastreados na visualização de Histórico NÃO irão ser deletadas.

Tem certeza que quer proceder?

Segure a tecla Shift para pular esse diálogo no futuro.", + "desc": "Deletar esses {{objectLength}} objetos rastreados remove as capturas de imagem, quaisquer embeddings salvos, e quaisquer entradas do ciclo de vida associadas do objeto. Gravações desses objetos rastreados na visualização de Histórico NÃO irão ser deletadas.

Tem certeza que quer proceder?

Segure a tecla Shift para pular esse diálogo no futuro.", "toast": { "success": "Objetos rastreados deletados com sucesso.", "error": "Falha ao deletar objeto rastreado: {{errorMessage}}" @@ -122,5 +122,13 @@ "placeholder": "Digite para pesquisar por placas de identificação…", "noLicensePlatesFound": "Nenhuma placa de identificação encontrada.", "selectPlatesFromList": "Seleciona uma ou mais placas da lista." + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" } } diff --git a/web/public/locales/pt-BR/views/configEditor.json b/web/public/locales/pt-BR/views/configEditor.json index 1bd110a6f..46c4808cb 100644 --- a/web/public/locales/pt-BR/views/configEditor.json +++ b/web/public/locales/pt-BR/views/configEditor.json @@ -12,5 +12,7 @@ "error": { "savingError": "Erro ao salvar configuração" } - } + }, + "safeConfigEditor": "Editor de Configuração (Modo Seguro)", + "safeModeDescription": "O Frigate está no modo seguro devido a um erro de validação de configuração." } diff --git a/web/public/locales/pt-BR/views/events.json b/web/public/locales/pt-BR/views/events.json index 1cd63daf0..2e7eac4cb 100644 --- a/web/public/locales/pt-BR/views/events.json +++ b/web/public/locales/pt-BR/views/events.json @@ -26,13 +26,15 @@ }, "markTheseItemsAsReviewed": "Marque estes itens como revisados", "newReviewItems": { - "button": "Novos Itens para Revisão", + "button": "Novos Itens para Revisar", "label": "Ver novos itens para revisão" }, "selected_one": "{{count}} selecionado(s)", - "documentTitle": "Revisão - Frigate", + "documentTitle": "Revisar - Frigate", "markAsReviewed": "Marcar como Revisado", "selected_other": "{{count}} selecionado(s)", "camera": "Câmera", - "detected": "detectado" + "detected": "detectado", + "suspiciousActivity": "Atividade Suspeita", + "threateningActivity": "Atividade de Ameaça" } diff --git a/web/public/locales/pt-BR/views/explore.json b/web/public/locales/pt-BR/views/explore.json index a43ee2b17..3161e4a5f 100644 --- a/web/public/locales/pt-BR/views/explore.json +++ b/web/public/locales/pt-BR/views/explore.json @@ -3,15 +3,15 @@ "generativeAI": "IA Generativa", "exploreMore": "Explorar mais objetos {{label}}", "exploreIsUnavailable": { - "title": "Explorar não está disponível", + "title": "A seção Explorar está indisponível", "embeddingsReindexing": { - "context": "Explorar pode ser usado depois da incorporação do objeto rastreado terminar a reindexação.", - "startingUp": "Começando…", - "estimatedTime": "Time estimado faltando:", - "finishingShortly": "Terminando em breve", + "context": "O menu explorar pode ser usado após os embeddings de objetos rastreados terem terminado de reindexar.", + "startingUp": "Iniciando…", + "estimatedTime": "Tempo estimado restante:", + "finishingShortly": "Finalizando em breve", "step": { - "thumbnailsEmbedded": "Miniaturas incorporadas: ", - "descriptionsEmbedded": "Descrições incorporadas: ", + "thumbnailsEmbedded": "Miniaturas embedded: ", + "descriptionsEmbedded": "Descrições embedded: ", "trackedObjectsProcessed": "Objetos rastreados processados: " } }, @@ -24,7 +24,7 @@ "visionModelFeatureExtractor": "Extrator de características do modelo de visão" }, "tips": { - "context": "Você pode querer reindexar as incorporações de seus objetos rastreados uma vez que os modelos forem baixados.", + "context": "Você pode querer reindexar os embeddings de seus objetos rastreados uma vez que os modelos forem baixados.", "documentation": "Leia a documentação" }, "error": "Um erro ocorreu. Verifique os registos do Frigate." @@ -49,12 +49,14 @@ "success": { "regenerate": "Uma nova descrição foi solicitada do {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para regenerar.", "updatedSublabel": "Sub-categoria atualizada com sucesso.", - "updatedLPR": "Placa de identificação atualizada com sucesso." + "updatedLPR": "Placa de identificação atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio requisitada com sucesso." }, "error": { "regenerate": "Falha ao ligar para {{provider}} para uma descrição nova: {{errorMessage}}", "updatedSublabelFailed": "Falha ao atualizar sub-categoria: {{errorMessage}}", - "updatedLPRFailed": "Falha ao atualizar placa de identificação: {{errorMessage}}" + "updatedLPRFailed": "Falha ao atualizar placa de identificação: {{errorMessage}}", + "audioTranscription": "Falha ao requisitar transcrição de áudio: {{errorMessage}}" } } }, @@ -99,6 +101,9 @@ "tips": { "descriptionSaved": "Descrição salva com sucesso", "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Pontuação" } }, "trackedObjectDetails": "Detalhes do Objeto Rastreado", @@ -184,12 +189,20 @@ }, "deleteTrackedObject": { "label": "Deletar esse objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicionar um gatilho para esse objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" } }, "dialog": { "confirmDelete": { "title": "Confirmar Exclusão", - "desc": "Deletar esse objeto rastreado remove a captura de imagem, qualquer embedding salvo, e quaisquer entradas de ciclo de vida de objeto associadas. Gravações desse objeto rastreado na visualização de Histórico NÃO serão deletadas.

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

Tem certeza que quer prosseguir?" } }, "noTrackedObjects": "Nenhum Objeto Rastreado Encontrado", @@ -205,5 +218,11 @@ "error": "Falha ao detectar objeto rastreado {{errorMessage}}" } } + }, + "aiAnalysis": { + "title": "Análise de IA" + }, + "concerns": { + "label": "Preocupações" } } diff --git a/web/public/locales/pt-BR/views/faceLibrary.json b/web/public/locales/pt-BR/views/faceLibrary.json index d08b38110..912236cf4 100644 --- a/web/public/locales/pt-BR/views/faceLibrary.json +++ b/web/public/locales/pt-BR/views/faceLibrary.json @@ -38,8 +38,8 @@ "deleteFaceAttempts": { "title": "Apagar Rostos", "desc_one": "Você tem certeza que quer deletar {{count}} rosto? Essa ação não pode ser desfeita.", - "desc_many": "Você tem certeza que quer deletar {{count}} rostos? Essa ação não pode ser desfeita.", - "desc_other": "" + "desc_many": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita.", + "desc_other": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita." }, "renameFace": { "title": "Renomear Rosto", diff --git a/web/public/locales/pt-BR/views/live.json b/web/public/locales/pt-BR/views/live.json index 97ca4675c..71f5ad625 100644 --- a/web/public/locales/pt-BR/views/live.json +++ b/web/public/locales/pt-BR/views/live.json @@ -43,6 +43,14 @@ "out": { "label": "Diminuir Zoom na câmera PTZ" } + }, + "focus": { + "in": { + "label": "Aumentar foco da câmera PTZ" + }, + "out": { + "label": "Tirar foco da câmera PTZ" + } } }, "camera": { @@ -135,7 +143,8 @@ "recording": "Gravação", "snapshots": "Capturas de Imagem", "audioDetection": "Detecção de Áudio", - "autotracking": "Auto Rastreamento" + "autotracking": "Auto Rastreamento", + "transcription": "Transcrição de Áudio" }, "history": { "label": "Exibir gravação histórica" @@ -154,5 +163,9 @@ "label": "Editar Grupo de Câmera" }, "exitEdit": "Sair da Edição" + }, + "transcription": { + "enable": "Habilitar Transcrição de Áudio em Tempo Real", + "disable": "Desabilitar Transcrição de Áudio em Tempo Real" } } diff --git a/web/public/locales/pt-BR/views/settings.json b/web/public/locales/pt-BR/views/settings.json index c5e0af438..4d4946b0a 100644 --- a/web/public/locales/pt-BR/views/settings.json +++ b/web/public/locales/pt-BR/views/settings.json @@ -20,7 +20,7 @@ "frigateplus": "Frigate+", "motionTuner": "Ajuste de Movimento", "debug": "Depurar", - "enrichments": "Melhorias" + "enrichments": "Enriquecimentos" }, "dialog": { "unsavedChanges": { @@ -37,12 +37,12 @@ "liveDashboard": { "title": "Painel em Tempo Real", "automaticLiveView": { - "label": "Visão em Tempo Real Automática", - "desc": "Automaticamente alterar para a visão em tempo real da câmera quando alguma atividade for detectada. Desativar essa opção faz com que as imagens estáticas da câmera no Painel em Tempo Real atualizem apenas uma vez por minuto." + "label": "Visualização em Tempo Real Automática", + "desc": "Automaticamente alterar para a visualização em tempo real da câmera quando alguma atividade for detectada. Desativar essa opção faz com que as imagens estáticas da câmera no Painel em Tempo Real atualizem apenas uma vez por minuto." }, "playAlertVideos": { "label": "Reproduzir Alertas de Video", - "desc": "Por padrão, alertas recentes no Painel em Tempo Real sejam reproduzidos como vídeos em loop. Desative essa opção para mostrar apenas a imagens estáticas de alertas recentes nesse dispositivo / navegador." + "desc": "Por padrão, alertas recentes no Painel em Tempo Real são reproduzidos como vídeos em loop. Desative essa opção para mostrar apenas a imagens estáticas de alertas recentes nesse dispositivo / navegador." } }, "storedLayouts": { @@ -58,8 +58,8 @@ "recordingsViewer": { "title": "Visualizador de Gravações", "defaultPlaybackRate": { - "label": "Taxa Padrão de Reprodução", - "desc": "Taxa Padrão de Reprodução para Gravações." + "label": "Velocidade Padrão de Reprodução", + "desc": "Velocidade padrão de reprodução para gravações." } }, "calendar": { @@ -87,7 +87,7 @@ "unsavedChanges": "Alterações de configurações de Enriquecimento não salvas", "birdClassification": { "title": "Classificação de Pássaros", - "desc": "A classificação de pássaros identifica pássaros conhecidos usando o modelo Tensorflow quantizado. Quando um pássaro é reconhecido, o seu nome commum será adicionado como uma subcategoria. Essa informação é incluida na UI, filtros e notificações." + "desc": "A classificação de pássaros identifica pássaros conhecidos usando o modelo Tensorflow quantizado. Quando um pássaro é reconhecido, o seu nome comum será adicionado como um sub-rótulo. Essa informação é incluida na UI, filtros e notificações." }, "semanticSearch": { "title": "Busca Semântica", @@ -95,7 +95,7 @@ "readTheDocumentation": "Leia a Documentação", "reindexNow": { "label": "Reindexar Agora", - "desc": "A reindexação irá regenerar os embeddings para todos os objetos rastreados. Esse processo roda em segundo plano e pode 100% da CPU e levar um tempo considerável dependendo do número de objetos rastreados que você possui.", + "desc": "A reindexação irá regenerar os embeddings para todos os objetos rastreados. Esse processo roda em segundo plano e pode demandar 100% da CPU e levar um tempo considerável dependendo do número de objetos rastreados que você possui.", "confirmTitle": "Confirmar Reindexação", "confirmDesc": "Tem certeza que quer reindexar todos os embeddings de objetos rastreados? Esse processo rodará em segundo plano porém utilizará 100% da CPU e levará uma quantidade de tempo considerável. Você pode acompanhar o progresso na página Explorar.", "confirmButton": "Reindexar", @@ -108,7 +108,7 @@ "desc": "O tamanho do modelo usado para embeddings de pesquisa semântica.", "small": { "title": "pequeno", - "desc": "Usandopequeno emprega a versão quantizada do modelo que utiliza menos RAM e roda mais rápido na CPU, com diferenças negligíveis na qualidade dos embeddings." + "desc": "Usando pequeno emprega a versão quantizada do modelo que utiliza menos RAM e roda mais rápido na CPU, com diferenças negligíveis na qualidade dos embeddings." }, "large": { "title": "grande", @@ -118,24 +118,24 @@ }, "faceRecognition": { "title": "Reconhecimento Facial", - "desc": "O reconhecimento facial permite que pessoas sejam associadas a nomes e quando seus rostos forem reconhecidos, o Frigate associará o nome da pessoa como uma sub-categoria. Essa informação é inclusa na UI, filtros e notificações.", + "desc": "O reconhecimento facial permite que pessoas sejam associadas a nomes e quando seus rostos forem reconhecidos, o Frigate associará o nome da pessoa como um sub-rótulo. Essa informação é inclusa na UI, filtros e notificações.", "readTheDocumentation": "Leia a Documentação", "modelSize": { "label": "Tamanho do Modelo", "desc": "O tamanho do modelo usado para reconhecimento facial.", "small": { "title": "pequeno", - "desc": "Usar pequeno emprega o modelo de embedding de rosto FaceNet, que roda de maneira eficiente na maioria das CPUs." + "desc": "Usar o pequeno emprega o modelo de embedding de rosto FaceNet, que roda de maneira eficiente na maioria das CPUs." }, "large": { "title": "grande", - "desc": "Usando o grande emprega um modelo de embedding de rosto ArcFace e irá automáticamente roda pela GPU se aplicável." + "desc": "Usar o grande emprega um modelo de embedding de rosto ArcFace e irá automáticamente rodar pela GPU se aplicável." } } }, "licensePlateRecognition": { "title": "Reconhecimento de Placa de Identificação", - "desc": "O Frigate pode reconhecer placas de identificação em veículos e automáticamente adicionar os caracteres detectados ao campo placas_de_identificação_reconhecidas ou um nome conhecido como uma sub-categoria a objetos que são do tipo carro. Um uso típico é ler a placa de carros entrando em uma garagem ou carros passando pela rua.", + "desc": "O Frigate pode reconhecer placas de identificação em veículos e automáticamente adicionar os caracteres detectados ao campo placas_de_identificação_reconhecidas ou um nome conhecido como um sub-rótulo a objetos que são do tipo carro. Um uso típico é ler a placa de carros entrando em uma garagem ou carros passando pela rua.", "readTheDocumentation": "Leia a Documentação" }, "restart_required": "Necessário reiniciar (configurações de enriquecimento foram alteradas)", @@ -148,22 +148,22 @@ "title": "Configurações de Câmera", "streams": { "title": "Transmissões", - "desc": "Temporáriamente desativar a câmera até o Frigate reiniciar. Desatiar a câmera completamente impede o processamento da transmissão dessa câmera pelo Frigate. Detecções, gravações e depuração estarão indisponíveis.
Nota: Isso não desativa as retransmissões do go2rtc." + "desc": "Temporáriamente desativa a câmera até o Frigate reiniciar. Desativar a câmera completamente impede o processamento da transmissão dessa câmera pelo Frigate. Detecções, gravações e depuração estarão indisponíveis.
Nota: Isso não desativa as retransmissões do go2rtc." }, "review": { "title": "Revisar", - "desc": "Temporariamente habilitar/desabilitar alertas e detecções para essa câmera até o Frigate reiniciar. Quando desabilitado, nenhum novo item de revisão será gerado. ", + "desc": "Temporariamente habilita/desabilita alertas e detecções para essa câmera até o Frigate reiniciar. Quando desabilitado, nenhum novo item de revisão será gerado. ", "alerts": "Alertas ", "detections": "Detecções " }, "reviewClassification": { - "title": "Revisar Classificação", - "desc": "O Frigate categoriza itens de revisão como Alertas e Detecções. Por padrão, todas as pessoa e carros são considerados alertas. Você pode refinar a categorização dos seus itens revisados configurando as zonas requeridas para eles.", + "title": "Classificação de Revisões", + "desc": "O Frigate categoriza itens de revisão como Alertas e Detecções. Por padrão, todas as pessoas e carros são considerados alertas. Você pode refinar a categorização dos seus itens revisados configurando as zonas requeridas para eles.", "readTheDocumentation": "Leia a Documentação", "noDefinedZones": "Nenhuma zona definida para essa câmera.", "selectAlertsZones": "Selecionar as zonas para Alertas", "selectDetectionsZones": "Selecionar as zonas para Detecções", - "objectAlertsTips": "Todos os {{alertsLabels}} objetos em {{cameraName}} serão exibidos como Alertas.", + "objectAlertsTips": "Todos os objetos {{alertsLabels}} em {{cameraName}} serão exibidos como Alertas.", "zoneObjectAlertsTips": "Todos os {{alertsLabels}} objetos detectados em {{zone}} em {{cameraName}} serão exibidos como Alertas.", "objectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados em {{cameraName}} serão exibidos como Detecções independente de qual zona eles estiverem.", "zoneObjectDetectionsTips": { @@ -176,6 +176,43 @@ "toast": { "success": "A configuração de Revisão de Classificação foi salva. Reinicie o Frigate para aplicar as mudanças." } + }, + "object_descriptions": { + "title": "Descrições de Objeto por IA Generativa", + "desc": "Habilitar descrições por IA Generativa temporariamente para essa câmera. Quando desativada, as descrições geradas por IA não serão requisitadas para objetos rastreados para essa câmera." + }, + "review_descriptions": { + "title": "Revisar Descrições de IA Generativa", + "desc": "Habilitar/desabilitar temporariamente descrições de revisão de IA Generativa para essa câmera. Quando desativada, as descrições de IA Generativa não serão solicitadas para revisão para essa câmera." + }, + "addCamera": "Adicionar Câmera Nova", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurções de Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as opções da câmera incluindo as de transmissão e papéis.", + "name": "Nome da Câmera", + "nameRequired": "Nome para a câmera é requerido", + "nameInvalid": "O nome da câmera deve contar apenas letras, números, sublinhado ou hífens", + "namePlaceholder": "ex: porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Transmissões de Entrada", + "path": "Caminho da Transmissão", + "pathRequired": "Um caminho para a transmissão é requerido", + "pathPlaceholder": "rtsp://...", + "roles": "Regras", + "rolesRequired": "Ao menos um papel é requerido", + "rolesUnique": "Cada papel (áudio, detecção, gravação) pode ser atribuído a uma única transmissão", + "addInput": "Adicionar Transmissão de Entrada", + "removeInput": "Remover Transmissão de Entrada", + "inputsRequired": "Ao menos uma transmissão de entrada é requerida" + }, + "toast": { + "success": "Câmera {{cameraName}} salva com sucesso" + } } }, "masksAndZones": { @@ -421,6 +458,11 @@ "timestamp": { "title": "Timestamp", "desc": "Sobreponha um timestamp na imagem" + }, + "paths": { + "title": "Caminho", + "desc": "Mostrar pontos significantes do caminho do objeto rastreado", + "tips": "

Caminhos


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

" } }, "users": { @@ -618,5 +660,100 @@ "success": "As configurações do Frigate+ foram salvas. Reinicie o Frigate para aplicar as alterações.", "error": "Falha ao salvar as alterações de configuração: {{errorMessage}}" } + }, + "triggers": { + "documentTitle": "Gatilhos", + "management": { + "title": "Gerenciamento de Gatilhos", + "desc": "Gerenciar gatilhos para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes para os seus objetos rastreados selecionados, e o tipo de descrição para acionar descrições semelhantes para textos que você especifica." + }, + "addTrigger": "Adicionar Gatilho", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limiar", + "actions": "Ações", + "noTriggers": "Nenhum gatilho configurado para essa câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Gatilho", + "lastTriggered": "Acionado pela última vez" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Gatilho", + "desc": "Criar gatilho para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Gatilho", + "desc": "Editar as configurações de gatilho na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Gatilho", + "desc": "Tem certeza que quer deletar o gatilho {{triggerName}}? Essa ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Digite o nome do gatilho", + "error": { + "minLength": "O nome precisa ter no mínimo 2 caracteres.", + "invalidCharacters": "O nome pode contar apenas letras, números, sublinhados, e hífens.", + "alreadyExists": "Um gatilho com esse nome já existe para essa câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar esse gatilho" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecionar o tipo de gatilho" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecionar uma imagem", + "textPlaceholder": "Digitar conteúdo do texto", + "imageDesc": "Selecionar uma imagem para acionar essa ação quando uma imagem semelhante for detectada.", + "textDesc": "Digite o texto para ativar essa ação quando uma descrição semelhante de objeto rastreado for detectada.", + "error": { + "required": "Um conteúdo é requerido." + } + }, + "threshold": { + "title": "Limiar", + "error": { + "min": "O limitar deve ser no mínimo 0", + "max": "O limiar deve ser no mínimo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate dispara uma mensagem MQTT para todos os gatilhos. Escolha uma ação adicional para realizar quando uma ação for disparada.", + "error": { + "min": "Ao menos uma ação deve ser selecionada." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Gatilho {{name}} criado com sucesso.", + "updateTrigger": "Gatilho {{name}} atualizado com sucesso.", + "deleteTrigger": "Gatilho {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar gatilho: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar gatilho: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar gatilho: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/pt-BR/views/system.json b/web/public/locales/pt-BR/views/system.json index 74a2c4564..36ee37345 100644 --- a/web/public/locales/pt-BR/views/system.json +++ b/web/public/locales/pt-BR/views/system.json @@ -157,7 +157,7 @@ "detectHighCpuUsage": "{{camera}} possui alta utilização de CPU para detecção ({{detectAvg}}%)", "healthy": "O sistema está saudável", "cameraIsOffline": "{{camera}} está offline", - "reindexingEmbeddings": "Reindexando os vetores de característica de imagens ({{processed}}% completado)", + "reindexingEmbeddings": "Reindexando os embeddings ({{processed}}% completado)", "detectIsSlow": "{{detect}} está lento ({{speed}} ms)" }, "enrichments": { @@ -167,13 +167,13 @@ "face_recognition": "Reconhecimento Facial", "plate_recognition": "Reconhecimento de Placa", "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", - "text_embedding_speed": "Velocidade de Geração de Vetores de Texto", + "text_embedding_speed": "Velocidade de Embeddings de Texto", "yolov9_plate_detection_speed": "Velocidade de Reconhecimento de Placas do YOLOv9", "yolov9_plate_detection": "Detecção de Placas do YOLOv9", - "image_embedding": "Vetores de Características de Imagens", - "text_embedding": "Vetor de Característica de Texto", - "image_embedding_speed": "Velocidade de Geração de Vetores de Imagem", - "face_embedding_speed": "Velocidade de Geração de Vetores de Rostos", + "image_embedding": "Embeddings de Imagens", + "text_embedding": "Embeddings de Texto", + "image_embedding_speed": "Velocidade de Embeddings de Imagens", + "face_embedding_speed": "Velocidade de Embedding de Rostos", "face_recognition_speed": "Velocidade de Reconhecimento de Rostos" } } diff --git a/web/public/locales/pt/common.json b/web/public/locales/pt/common.json index ad63195c1..69d301472 100644 --- a/web/public/locales/pt/common.json +++ b/web/public/locales/pt/common.json @@ -201,7 +201,15 @@ "hu": "Húngaro (Magyar)", "sk": "Eslovaco (Slovenčina)", "th": "Tailandês", - "ca": "Català (Catalão)" + "ca": "Català (Catalão)", + "ptBR": "Português brasileiro", + "sr": "Српски (Sérvio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego", + "id": "Bahasa Indonesia (Indonésio)", + "ur": "اردو (Urdu)" }, "appearance": "Aparência", "darkMode": { @@ -271,5 +279,6 @@ "desc": "Página não encontrada", "title": "404" }, - "selectItem": "Selecionar {{item}}" + "selectItem": "Selecionar {{item}}", + "readTheDocumentation": "Leia a documentação" } diff --git a/web/public/locales/pt/components/dialog.json b/web/public/locales/pt/components/dialog.json index 766711539..4f834b78e 100644 --- a/web/public/locales/pt/components/dialog.json +++ b/web/public/locales/pt/components/dialog.json @@ -122,5 +122,12 @@ "markAsReviewed": "Marcar como analisado", "deleteNow": "Excluir agora" } + }, + "imagePicker": { + "selectImage": "Selecione a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por etiqueta ou sub-etiqueta..." + }, + "noImages": "Nenhuma miniatura encontrada para esta câmera" } } diff --git a/web/public/locales/pt/components/filter.json b/web/public/locales/pt/components/filter.json index 53f56241f..30f2bd24f 100644 --- a/web/public/locales/pt/components/filter.json +++ b/web/public/locales/pt/components/filter.json @@ -123,5 +123,13 @@ "loadFailed": "Falha ao carregar as placas reconhecidas.", "loading": "Carregando placas reconhecidas…", "placeholder": "Digite para procurar placas…" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" } } diff --git a/web/public/locales/pt/views/configEditor.json b/web/public/locales/pt/views/configEditor.json index 6d6c98166..916946517 100644 --- a/web/public/locales/pt/views/configEditor.json +++ b/web/public/locales/pt/views/configEditor.json @@ -12,5 +12,7 @@ } }, "documentTitle": "Editor de configuração - Frigate", - "confirm": "Sair sem salvar?" + "confirm": "Sair sem salvar?", + "safeConfigEditor": "Editor de Configurações (Modo de Segurança)", + "safeModeDescription": "O Frigate está em modo de segurança devido a um erro de validação de configuração." } diff --git a/web/public/locales/pt/views/events.json b/web/public/locales/pt/views/events.json index 6478001c6..c4e03b788 100644 --- a/web/public/locales/pt/views/events.json +++ b/web/public/locales/pt/views/events.json @@ -34,5 +34,6 @@ "camera": "Câmara", "detected": "detectado", "selected_one": "{{count}} selecionado", - "selected_other": "{{count}} selecionados" + "selected_other": "{{count}} selecionados", + "suspiciousActivity": "Atividade Suspeita" } diff --git a/web/public/locales/pt/views/explore.json b/web/public/locales/pt/views/explore.json index a271d1df7..541e3b802 100644 --- a/web/public/locales/pt/views/explore.json +++ b/web/public/locales/pt/views/explore.json @@ -43,12 +43,14 @@ "success": { "regenerate": "Uma nova descrição foi solicitada pelo {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para ser regenerada.", "updatedSublabel": "Sub-rotulo atualizado com sucesso.", - "updatedLPR": "Matrícula atualizada com sucesso." + "updatedLPR": "Matrícula atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio solicitada com sucesso." }, "error": { "regenerate": "Falha ao chamar {{provider}} para uma nova descrição: {{errorMessage}}", "updatedSublabelFailed": "Falha ao atualizar o sub-rotulo: {{errorMessage}}", - "updatedLPRFailed": "Falha ao atualizar a matrícula: {{errorMessage}}" + "updatedLPRFailed": "Falha ao atualizar a matrícula: {{errorMessage}}", + "audioTranscription": "Falha ao solicitar transcrição de áudio: {{errorMessage}}" } }, "button": { @@ -97,6 +99,9 @@ "tips": { "descriptionSaved": "Descrição salva com sucesso", "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Classificação" } }, "documentTitle": "Explorar - Frigate", @@ -183,6 +188,14 @@ }, "deleteTrackedObject": { "label": "Excluir este objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicione um gatilho para este objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" } }, "searchResult": { @@ -205,5 +218,8 @@ "trackedObjectsCount_one": "{{count}} objeto rastreado ", "trackedObjectsCount_many": "{{count}} objetos rastreados ", "trackedObjectsCount_other": "", - "exploreMore": "Explora mais objetos {{label}}" + "exploreMore": "Explora mais objetos {{label}}", + "aiAnalysis": { + "title": "Análise IA" + } } diff --git a/web/public/locales/pt/views/live.json b/web/public/locales/pt/views/live.json index eb0330a97..770028a85 100644 --- a/web/public/locales/pt/views/live.json +++ b/web/public/locales/pt/views/live.json @@ -42,6 +42,14 @@ "center": { "label": "Clique no quadro para centralizar a câmara PTZ" } + }, + "focus": { + "in": { + "label": "Em foco da câmera PTZ" + }, + "out": { + "label": "Fora foco da câmera PTZ em" + } } }, "lowBandwidthMode": "Modo de baixa largura de banda", @@ -130,7 +138,8 @@ "recording": "Gravando", "audioDetection": "Detecção de áudio", "autotracking": "Rastreamento automático", - "snapshots": "Snapshots" + "snapshots": "Snapshots", + "transcription": "Transcrição de áudio" }, "effectiveRetainMode": { "modes": { @@ -154,5 +163,9 @@ }, "history": { "label": "Mostrar filmagens históricas" + }, + "transcription": { + "enable": "Habilitar transcrição de áudio ao vivo", + "disable": "Desabilitar transcrição de áudio ao vivo" } } diff --git a/web/public/locales/pt/views/settings.json b/web/public/locales/pt/views/settings.json index f453e6a5b..391302261 100644 --- a/web/public/locales/pt/views/settings.json +++ b/web/public/locales/pt/views/settings.json @@ -6,7 +6,7 @@ "motionTuner": "Ajuste de movimento - Frigate", "object": "Depuração - Frigate", "authentication": "Configurações de autenticação - Frigate", - "general": "Configurações Gerais - Frigate", + "general": "Configurações gerais - Frigate", "frigatePlus": "Configurações do Frigate+ - Frigate", "default": "Configurações - Frigate", "notifications": "Configuração de Notificações - Frigate", @@ -465,6 +465,11 @@ "mask": { "title": "Máscaras de movimento", "desc": "Mostrar polígonos de máscara de movimento" + }, + "paths": { + "title": "Caminhos", + "desc": "Mostrar pontos significativos do caminho do objeto rastreado", + "tips": "

Paths


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

" } }, "camera": { @@ -499,6 +504,43 @@ "desc": "Ative ou desative alertas e detecções para esta câmara. Quando desativado, nenhum novo item de análise será gerado. ", "alerts": "Alertas ", "detections": "Detecções " + }, + "object_descriptions": { + "title": "Descrições de objetos de IA generativa", + "desc": "Ative/desative temporariamente as descrições de objetos de IA generativa para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para objetos rastreados nesta câmera." + }, + "review_descriptions": { + "title": "Descrições de análises de IA generativa", + "desc": "Ative/desative temporariamente as descrições de avaliação geradas por IA para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para itens de avaliação nesta câmera." + }, + "addCamera": "Adicionar Nova Câmera", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurações da Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as definições da câmera, incluindo entradas de transmissão e funções.", + "name": "Nome da Câmera", + "nameRequired": "O nome da câmera é obrigatório", + "nameInvalid": "O nome da câmera deve conter apenas letras, números, sublinhados ou hifens", + "namePlaceholder": "e.g., porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Entrada de Streams", + "path": "Caminho da Stream", + "pathRequired": "Caminho da Stream é obrigatória", + "pathPlaceholder": "rtsp://...", + "roles": "Funções", + "rolesRequired": "Pelo menos uma função é necessária", + "rolesUnique": "Cada função (áudio, detecção, gravação) só pode ser atribuída a uma stream", + "addInput": "Adicionar Entrada de Stream", + "removeInput": "Remover Entrada de Stream", + "inputsRequired": "É necessário pelo menos uma stream de entrada" + }, + "toast": { + "success": "Câmera {{cameraName}} guardada com sucesso" + } } }, "motionDetectionTuner": { @@ -682,5 +724,100 @@ "roleUpdateFailed": "Falha ao atualizar a função: {{errorMessage}}" } } + }, + "triggers": { + "documentTitle": "Triggers (gatilhos)", + "management": { + "title": "Gestão de Triggers", + "desc": "Gira triggers para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes ao objeto rastreado selecionado e o tipo de descrição para acionar descrições semelhantes ao texto especificado." + }, + "addTrigger": "Adicionar Trigger", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limite", + "actions": "Ações", + "noTriggers": "Nenhum trigger configurado para esta câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Trigger", + "lastTriggered": "Último acionado" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Trigger", + "desc": "Crie um trigger para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Trigger", + "desc": "Editar as definições do trigger na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Trigger", + "desc": "Tem certeza de que deseja apagar o trigger {{triggerName}}? Esta ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Insira o nome do trigger", + "error": { + "minLength": "O nome deve ter pelo menos 2 caracteres.", + "invalidCharacters": "O nome só pode conter letras, números, sublinhados e hifens.", + "alreadyExists": "Já existe um trigger com este nome para esta câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar este trigger" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecione o tipo de trigger" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecione uma imagem", + "textPlaceholder": "Insira o conteúdo do texto", + "imageDesc": "Selecione uma imagem para acionar esta ação quando uma imagem semelhante for detectada.", + "textDesc": "Insira um texto para acionar esta ação quando uma descrição de objeto rastreado semelhante for detectada.", + "error": { + "required": "O Conteúdo é obrigatório." + } + }, + "threshold": { + "title": "Limite", + "error": { + "min": "Limite deve ser pelo menos 0", + "max": "Limite deve ser no máximo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate envia uma mensagem MQTT para todos os triggers. Escolha uma ação adicional a ser executada quando este trigger for disparado.", + "error": { + "min": "Pelo menos uma ação deve ser selecionada." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} criado com sucesso.", + "updateTrigger": "Trigger {{name}} atualizado com sucesso.", + "deleteTrigger": "Trigger {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar trigger: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar o trigger: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar o trigger: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/ro/common.json b/web/public/locales/ro/common.json index 145d511a4..1e36296f6 100644 --- a/web/public/locales/ro/common.json +++ b/web/public/locales/ro/common.json @@ -42,7 +42,7 @@ "24hour": "dd-MM-yy-HH-mm-ss" }, "30minutes": "30 de minute", - "1hour": "O oră", + "1hour": "1 oră", "12hours": "12 ore", "24hours": "24 de ore", "pm": "PM", @@ -123,7 +123,15 @@ "ro": "Română (Română)", "hu": "Magyar (Maghiară)", "fi": "Suomi (Finlandeză)", - "th": "ไทย (Thailandeză)" + "th": "ไทย (Thailandeză)", + "ptBR": "Português brasileiro (Portugheză braziliană)", + "sr": "Српски (Sârbă)", + "sl": "Slovenščina (Slovenă)", + "lt": "Lietuvių (Lituaniană)", + "bg": "Български (Bulgară)", + "gl": "Galego (Galiciană)", + "id": "Bahasa Indonesia (Indoneziană)", + "ur": "اردو (Urdu)" }, "theme": { "default": "Implicit", @@ -261,5 +269,6 @@ "documentTitle": "Nu a fost găsit - Frigate", "title": "404", "desc": "Pagină negăsită" - } + }, + "readTheDocumentation": "Citește documentația" } diff --git a/web/public/locales/ro/components/dialog.json b/web/public/locales/ro/components/dialog.json index c07b2cee0..626c30727 100644 --- a/web/public/locales/ro/components/dialog.json +++ b/web/public/locales/ro/components/dialog.json @@ -122,5 +122,12 @@ } } } + }, + "imagePicker": { + "selectImage": "Selectează miniatura unui obiect urmărit", + "search": { + "placeholder": "Caută după etichetă sau subetichetă..." + }, + "noImages": "Nu s-au găsit miniaturi pentru această cameră" } } diff --git a/web/public/locales/ro/components/filter.json b/web/public/locales/ro/components/filter.json index 40c0c593c..9bce2b1ed 100644 --- a/web/public/locales/ro/components/filter.json +++ b/web/public/locales/ro/components/filter.json @@ -122,5 +122,13 @@ "loading": "Se încarcă numerele de înmatriculare recunoscute…", "placeholder": "Caută plăcuțe de înmatriculare…", "loadFailed": "Nu s-au putut încărca numerele de înmatriculare recunoscute." + }, + "classes": { + "label": "Clase", + "all": { + "title": "Toate clasele" + }, + "count_one": "{{count}} Clasă", + "count_other": "{{count}} Clase" } } diff --git a/web/public/locales/ro/views/configEditor.json b/web/public/locales/ro/views/configEditor.json index cecfb7cc7..21f7d4769 100644 --- a/web/public/locales/ro/views/configEditor.json +++ b/web/public/locales/ro/views/configEditor.json @@ -1,5 +1,5 @@ { - "documentTitle": "Editor configurație - Frigate", + "documentTitle": "Editor de configurație - Frigate", "configEditor": "Editor de configurație", "copyConfig": "Copiază setările", "saveAndRestart": "Salvează și repornește", @@ -12,5 +12,7 @@ "savingError": "Eroare la salvarea setărilor" } }, - "confirm": "Ieși fără să salvezi?" + "confirm": "Ieși fără să salvezi?", + "safeConfigEditor": "Editor de configurație (mod de siguranță)", + "safeModeDescription": "Frigate este în modul de siguranță din cauza unei erori de validare a configurației." } diff --git a/web/public/locales/ro/views/events.json b/web/public/locales/ro/views/events.json index 30ae1ecb1..f250d6feb 100644 --- a/web/public/locales/ro/views/events.json +++ b/web/public/locales/ro/views/events.json @@ -34,5 +34,7 @@ "detections": "Detecții", "detected": "detectat", "selected_one": "{{count}} selectate", - "selected_other": "{{count}} selectate" + "selected_other": "{{count}} selectate", + "suspiciousActivity": "Activitate suspectă", + "threateningActivity": "Activitate amenințătoare" } diff --git a/web/public/locales/ro/views/explore.json b/web/public/locales/ro/views/explore.json index f9b4b0867..66e3abc29 100644 --- a/web/public/locales/ro/views/explore.json +++ b/web/public/locales/ro/views/explore.json @@ -103,12 +103,14 @@ "success": { "regenerate": "O nouă descriere a fost solicitată de la {{provider}}. În funcție de viteza furnizorului tău, regenerarea noii descrieri poate dura ceva timp.", "updatedSublabel": "Subeticheta a fost actualizată cu succes.", - "updatedLPR": "Plăcuța de înmatriculare a fost actualizată cu succes." + "updatedLPR": "Plăcuța de înmatriculare a fost actualizată cu succes.", + "audioTranscription": "Transcrierea audio a fost solicitată cu succes." }, "error": { "updatedSublabelFailed": "Nu s-a putut actualiza sub-etichetarea: {{errorMessage}}", "updatedLPRFailed": "Plăcuța de înmatriculare nu a putut fi actualizată: {{errorMessage}}", - "regenerate": "Eroare la apelarea {{provider}} pentru o nouă descriere: {{errorMessage}}" + "regenerate": "Eroare la apelarea {{provider}} pentru o nouă descriere: {{errorMessage}}", + "audioTranscription": "Solicitarea transcrierii audio a eșuat: {{errorMessage}}" } } }, @@ -153,7 +155,10 @@ }, "expandRegenerationMenu": "Extinde meniul de regenerare", "regenerateFromSnapshot": "Regenerează din snapshot", - "regenerateFromThumbnails": "Regenerează din miniaturi" + "regenerateFromThumbnails": "Regenerează din miniaturi", + "score": { + "label": "Scor" + } }, "exploreMore": "Explorează mai multe obiecte cu {{label}}", "trackedObjectDetails": "Detalii despre obiectul urmărit", @@ -187,6 +192,14 @@ "submitToPlus": { "label": "Trimite către Frigate+", "aria": "Trimite către Frigate Plus" + }, + "addTrigger": { + "label": "Adaugă declanșator", + "aria": "Adaugă un declanșator pentru acest obiect urmărit" + }, + "audioTranscription": { + "label": "Transcrie", + "aria": "Solicită transcrierea audio" } }, "dialog": { @@ -205,5 +218,11 @@ } }, "tooltip": "Potrivire {{type}} cu {{confidence}}%" + }, + "aiAnalysis": { + "title": "Analiză AI" + }, + "concerns": { + "label": "Îngrijorări" } } diff --git a/web/public/locales/ro/views/live.json b/web/public/locales/ro/views/live.json index 39ce37747..a998c49a8 100644 --- a/web/public/locales/ro/views/live.json +++ b/web/public/locales/ro/views/live.json @@ -39,7 +39,15 @@ "label": "Fă clic în cadru pentru a centra camera PTZ" } }, - "presets": "Presetări cameră PTZ" + "presets": "Presetări cameră PTZ", + "focus": { + "in": { + "label": "Focalizează camera PTZ în interior" + }, + "out": { + "label": "Focalizează camera PTZ în exterior" + } + } }, "cameraAudio": { "enable": "Activează sunetul camerei", @@ -135,7 +143,8 @@ "recording": "Înregistrare", "snapshots": "Snapshot-uri", "audioDetection": "Detectare sunet", - "autotracking": "Urmărire automată" + "autotracking": "Urmărire automată", + "transcription": "Transcriere audio" }, "history": { "label": "Afișează înregistrările istorice" @@ -154,5 +163,9 @@ "label": "Editează grupul de camere" }, "exitEdit": "Ieși din modul de editare" + }, + "transcription": { + "enable": "Activează transcrierea audio în timp real", + "disable": "Dezactivează transcrierea audio în timp real" } } diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json index fad64bd91..ce3258445 100644 --- a/web/public/locales/ro/views/settings.json +++ b/web/public/locales/ro/views/settings.json @@ -177,6 +177,43 @@ "selectAlertsZones": "Selectează zone pentru alerte", "noDefinedZones": "Nu sunt definite zone pentru această cameră.", "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor fi afișate ca alerte." + }, + "object_descriptions": { + "title": "Descrieri de obiecte generate de AI", + "desc": "Activează/dezactivează temporar descrierile de obiecte generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru obiectele urmărite pe această cameră." + }, + "review_descriptions": { + "title": "Descrieri de revizuiri generate de AI", + "desc": "Activează/dezactivează temporar descrierile recenziilor generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru elementele de recenzie de pe această cameră." + }, + "addCamera": "Adaugă cameră nouă", + "editCamera": "Editează camera:", + "selectCamera": "Selectează camera", + "backToSettings": "Înapoi la setările camerei", + "cameraConfig": { + "add": "Adaugă cameră", + "edit": "Editează camera", + "description": "Configurează setările camerei, inclusiv intrările de flux și rolurile.", + "name": "Numele camerei", + "nameRequired": "Numele camerei este obligatoriu", + "nameInvalid": "Numele camerei trebuie să conțină doar litere, cifre, underscore-uri sau cratime", + "namePlaceholder": "de ex.: usa_principala", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale stream", + "pathRequired": "Calea stream-ului este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detectare, înregistrare) poate fi atribuit doar unui singur stream", + "addInput": "Adaugă stream de intrare", + "removeInput": "Elimină stream-ul de intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "toast": { + "success": "Camera {{cameraName}} a fost salvată cu succes" + } } }, "masksAndZones": { @@ -399,6 +436,11 @@ "zones": { "title": "Zone", "desc": "Afișează conturul oricăror zone definite" + }, + "paths": { + "title": "Căi", + "desc": "Afișează punctele semnificative ale traseului obiectului urmărit", + "tips": "

Căi


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

" } }, "users": { @@ -619,5 +661,100 @@ "success": "Setările de mișcare au fost salvate." }, "title": "Reglaj detecție mișcare" + }, + "triggers": { + "documentTitle": "Declanșatoare", + "management": { + "title": "Gestionarea declanșatoarelor", + "desc": "Gestionează declanșatoarele pentru {{camera}}. Folosește tipul miniatură pentru a declanșa pe miniaturi similare cu obiectul urmărit selectat și tipul descriere pentru a declanșa pe descrieri similare textului pe care îl specifici." + }, + "addTrigger": "Adaugă declanșator", + "table": { + "name": "Nume", + "type": "Tip", + "content": "Conținut", + "threshold": "Prag", + "actions": "Acțiuni", + "noTriggers": "Nu sunt configurate declanșatoare pentru această cameră.", + "edit": "Editează", + "deleteTrigger": "Elimină declanșatorul", + "lastTriggered": "Ultima declanșare" + }, + "type": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "actions": { + "alert": "Marchează ca alertă", + "notification": "Trimite notificare" + }, + "dialog": { + "createTrigger": { + "title": "Crează declanșator", + "desc": "Creează un declanșator pentru camera {{camera}}" + }, + "editTrigger": { + "title": "Editează declanșatorul", + "desc": "Editează setările pentru declanșatorul de pe camera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimină declanșatorul", + "desc": "Ești sigur că vrei să ștergi declanșatorul {{triggerName}}? Această acțiune nu poate fi anulată." + }, + "form": { + "name": { + "title": "Nume", + "placeholder": "Introdu numele declanșatorului", + "error": { + "minLength": "Numele trebuie să aibă cel puțin 2 caractere.", + "invalidCharacters": "Numele poate conține doar litere, cifre, underscore-uri și cratime.", + "alreadyExists": "Un declanșator cu acest nume există deja pentru această cameră." + } + }, + "enabled": { + "description": "Activează sau dezactivează acest declanșator" + }, + "type": { + "title": "Tip", + "placeholder": "Selectează tipul de declanșator" + }, + "content": { + "title": "Conținut", + "imagePlaceholder": "Selectează o imagine", + "textPlaceholder": "Introdu conținutul textului", + "imageDesc": "Selectează o imagine pentru a declanșa această acțiune atunci când o imagine similară este detectată.", + "textDesc": "Introduceți textul pentru a declanșa această acțiune atunci când este detectată o descriere de obiect urmărit similară.", + "error": { + "required": "Conținutul este obligatoriu." + } + }, + "threshold": { + "title": "Prag", + "error": { + "min": "Pragul trebuie să fie cel puțin 0", + "max": "Pragul trebuie să fie cel mult 1" + } + }, + "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ă.", + "error": { + "min": "Trebuie selectată cel puțin o acțiune." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Declanșatorul {{name}} a fost creat cu succes.", + "updateTrigger": "Declanșatorul {{name}} a fost actualizat cu succes.", + "deleteTrigger": "Declanșatorul {{name}} a fost eliminat cu succes." + }, + "error": { + "createTriggerFailed": "Crearea declanșatorului a eșuat: {{errorMessage}}", + "updateTriggerFailed": "Actualizarea declanșatorului a eșuat: {{errorMessage}}", + "deleteTriggerFailed": "Eliminarea declanșatorului a eșuat: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/ro/views/system.json b/web/public/locales/ro/views/system.json index 5ba80df9c..4c9444ca5 100644 --- a/web/public/locales/ro/views/system.json +++ b/web/public/locales/ro/views/system.json @@ -77,7 +77,12 @@ }, "bandwidth": "Lățime de bandă" }, - "overview": "Prezentare generală" + "overview": "Prezentare generală", + "shm": { + "title": "Alocare SHM (memorie partajată)", + "warning": "Dimensiunea curentă a SHM de {{total}}MB este prea mică. Măriți-o la cel puțin {{min_shm}}MB.", + "readTheDocumentation": "Citește documentația" + } }, "title": "Sistem", "logs": { diff --git a/web/public/locales/ru/common.json b/web/public/locales/ru/common.json index 92ee6cf94..ee4a0df10 100644 --- a/web/public/locales/ru/common.json +++ b/web/public/locales/ru/common.json @@ -182,7 +182,15 @@ }, "yue": "粵語 (Кантонский)", "th": "ไทย (Тайский)", - "ca": "Català (Каталонский)" + "ca": "Català (Каталонский)", + "ptBR": "Português brasileiro (Бразильский португальский)", + "sr": "Српски (Сербский)", + "sl": "Slovenščina (Словенский)", + "lt": "Lietuvių (Литовский)", + "bg": "Български (Болгарский)", + "gl": "Galego (Галисийский)", + "id": "Bahasa Indonesia (Индонезийский)", + "ur": "اردو (Урду)" }, "darkMode": { "withSystem": { @@ -271,5 +279,7 @@ "admin": "Администратор", "viewer": "Наблюдатель", "desc": "Администраторы имеют полный доступ ко всем функциям в интерфейсе Frigate. Наблюдатели ограничены просмотром камер, элементов просмотра и архивных записей." - } + }, + "selectItem": "Выбрать {{item}}", + "readTheDocumentation": "Читать документацию" } diff --git a/web/public/locales/ru/components/dialog.json b/web/public/locales/ru/components/dialog.json index 078a37a97..748d079db 100644 --- a/web/public/locales/ru/components/dialog.json +++ b/web/public/locales/ru/components/dialog.json @@ -122,5 +122,12 @@ "markAsReviewed": "Пометить как просмотренное", "deleteNow": "Удалить сейчас" } + }, + "imagePicker": { + "search": { + "placeholder": "Искать по метке..." + }, + "selectImage": "Выбор миниатюры отслеживаемого объекта", + "noImages": "Не обнаружено миниатюр для этой камеры" } } diff --git a/web/public/locales/ru/components/filter.json b/web/public/locales/ru/components/filter.json index 024ebe02c..30d468812 100644 --- a/web/public/locales/ru/components/filter.json +++ b/web/public/locales/ru/components/filter.json @@ -123,5 +123,13 @@ }, "motion": { "showMotionOnly": "Показывать только движение" + }, + "classes": { + "label": "Классы", + "all": { + "title": "Все классы" + }, + "count_one": "{{count}} класс", + "count_other": "{{count}} классы" } } diff --git a/web/public/locales/ru/views/configEditor.json b/web/public/locales/ru/views/configEditor.json index 73b566a08..0dd775b24 100644 --- a/web/public/locales/ru/views/configEditor.json +++ b/web/public/locales/ru/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Ошибка сохранения конфигурации" } }, - "confirm": "Выйти без сохранения?" + "confirm": "Выйти без сохранения?", + "safeConfigEditor": "Редактор конфигурации (безопасный режим)", + "safeModeDescription": "Frigate находится в безопасном режиме из-за ошибки проверки конфигурации." } diff --git a/web/public/locales/sk/audio.json b/web/public/locales/sk/audio.json index 8a10ee24a..40f346066 100644 --- a/web/public/locales/sk/audio.json +++ b/web/public/locales/sk/audio.json @@ -47,5 +47,21 @@ "horse": "Kôň", "sheep": "Ovce", "camera": "Kamera", - "pant": "Oddychávanie" + "pant": "Oddychávanie", + "gargling": "Grganie", + "stomach_rumble": "Škvŕkanie v žalúdku", + "burping": "Grganie", + "skateboard": "Skateboard", + "hiccup": "Škytavka", + "fart": "Prd", + "hands": "Ruky", + "finger_snapping": "Lusknutie prstom", + "clapping": "Tlieskanie", + "heartbeat": "Tlkot srdca", + "heart_murmur": "Srdcový šelest", + "cheering": "Fandenie", + "applause": "Potlesk", + "chatter": "Chatárčenie", + "crowd": "Dav", + "children_playing": "Deti hrajúce sa" } diff --git a/web/public/locales/sk/common.json b/web/public/locales/sk/common.json index 28812e247..195b51392 100644 --- a/web/public/locales/sk/common.json +++ b/web/public/locales/sk/common.json @@ -39,7 +39,52 @@ "hour_few": "{{time}}hodiny", "hour_other": "{{time}}hodin", "m": "{{time}} min", - "s": "{{time}}s" + "s": "{{time}}s", + "minute_one": "{{time}}minuta", + "minute_few": "{{time}}minuty", + "minute_other": "{{time}}minut", + "second_one": "{{time}}sekunda", + "second_few": "{{time}}sekundy", + "second_other": "{{time}}sekund", + "formattedTimestamp": { + "12hour": "Deň MMM, h:mm:ss aaa", + "24hour": "Deň MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } }, - "selectItem": "Vyberte {{item}}" + "selectItem": "Vyberte {{item}}", + "unit": { + "speed": { + "mph": "mph" + } + }, + "readTheDocumentation": "Prečítajte si dokumentáciu" } diff --git a/web/public/locales/sk/components/camera.json b/web/public/locales/sk/components/camera.json index 151199d9a..37993420c 100644 --- a/web/public/locales/sk/components/camera.json +++ b/web/public/locales/sk/components/camera.json @@ -56,12 +56,31 @@ "continuousStreaming": { "label": "Nepretržité streamovanie", "desc": { - "title": "Obraz z kamery bude vždy vysielaný naživo, keď bude viditeľný na palubnej doske, aj keď nebude detekovaná žiadna aktivita." + "title": "Obraz z kamery bude vždy vysielaný naživo, keď bude viditeľný na palubnej doske, aj keď nebude detekovaná žiadna aktivita.", + "warning": "Nepretržité streamovanie môže spôsobiť vysoké využitie šírky pásma a problémy s výkonom. Používajte opatrne." } } } + }, + "compatibilityMode": { + "label": "Režim kompatibility", + "desc": "Túto možnosť povoľte iba v prípade, že živý prenos z vašej kamery zobrazuje farebné artefakty a na pravej strane obrazu sa nachádza diagonálna čiara." } } } + }, + "debug": { + "options": { + "label": "Nastavenia", + "title": "Možnosti", + "showOptions": "Zobraziť možnosti", + "hideOptions": "Skryť možnosti" + }, + "boundingBox": "Hranica", + "timestamp": "Časová pečiatka", + "zones": "Zóny", + "mask": "Maska", + "motion": "Pohyb", + "regions": "Kraje" } } diff --git a/web/public/locales/sk/components/dialog.json b/web/public/locales/sk/components/dialog.json index a254150e2..9b74ce48f 100644 --- a/web/public/locales/sk/components/dialog.json +++ b/web/public/locales/sk/components/dialog.json @@ -41,7 +41,10 @@ "end": { "title": "Čas ukončenia", "label": "Vybrat čas ukončenia" - } + }, + "lastHour_one": "Minulu hodinu", + "lastHour_few": "Minule{{count}}hodiny", + "lastHour_other": "Minulych{{count}}hodin" }, "name": { "placeholder": "Pomenujte Export" @@ -67,8 +70,48 @@ "restreaming": { "disabled": "Opätovné streamovanie nie je pre túto kameru povolené.", "desc": { - "title": "Pre ďalšie možnosti živého náhľadu a zvuku pre túto kameru nastavte go2rtc." + "title": "Pre ďalšie možnosti živého náhľadu a zvuku pre túto kameru nastavte go2rtc.", + "readTheDocumentation": "Prečítajte si dokumentáciu" + } + }, + "showStats": { + "label": "Zobraziť štatistiky streamu", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia" + }, + "search": { + "saveSearch": { + "label": "Uložiť vyhľadávanie", + "desc": "Zadajte názov pre toto uložené vyhľadávanie.", + "placeholder": "Zadajte názov pre vyhľadávanie", + "overwrite": "{{searchName}} už existuje. Uložením sa prepíše existujúca hodnota.", + "success": "Hľadanie ({{searchName}}) bolo uložené.", + "button": { + "save": { + "label": "Uložte toto vyhľadávanie" + } } } + }, + "recording": { + "confirmDelete": { + "title": "Potvrďte Odstrániť", + "desc": { + "selected": "Naozaj chcete odstrániť všetky nahrané videá spojené s touto položkou recenzie?

Podržte kláves Shift, aby ste v budúcnosti toto dialógové okno obišli." + }, + "toast": { + "success": "Videozáznam spojený s vybranými položkami recenzie bol úspešne odstránený.", + "error": "Nepodarilo sa odstrániť: {{error}}" + } + }, + "button": { + "export": "Exportovať", + "markAsReviewed": "Označiť ako skontrolované", + "deleteNow": "Odstrániť teraz" + } + }, + "imagePicker": { + "selectImage": "Výber miniatúry sledovaného objektu" } } diff --git a/web/public/locales/sk/components/filter.json b/web/public/locales/sk/components/filter.json index e1c1eb472..26bdae292 100644 --- a/web/public/locales/sk/components/filter.json +++ b/web/public/locales/sk/components/filter.json @@ -54,6 +54,46 @@ "relevance": "Relevantnosť" }, "cameras": { - "label": "Filter kamier" + "label": "Filter kamier", + "all": { + "title": "Všetky kamery", + "short": "Kamery" + } + }, + "classes": { + "label": "Triedy", + "all": { + "title": "Všetky triedy" + }, + "count_one": "Trieda {{count}}", + "count_other": "Triedy {{count}}" + }, + "review": { + "showReviewed": "Zobraziť skontrolované" + }, + "motion": { + "showMotionOnly": "Zobraziť len pohyb" + }, + "explore": { + "settings": { + "title": "Nastavenia", + "defaultView": { + "title": "Predvolené zobrazenie", + "desc": "Ak nie sú vybraté žiadne filtre, zobrazte súhrn naposledy sledovaných objektov pre každý štítok alebo zobrazte nefiltrovanú mriežku.", + "summary": "Zhrnutie", + "unfilteredGrid": "Nefiltrovaná mriežka" + }, + "gridColumns": { + "title": "Stĺpce mriežky", + "desc": "Vyberte počet stĺpcov v mriežkovom zobrazení." + }, + "searchSource": { + "label": "Vyhľadať zdroj", + "desc": "Vyberte, či chcete vyhľadávať v miniatúrach alebo v popisoch sledovaných objektov.", + "options": { + "thumbnailImage": "Obrázok miniatúry" + } + } + } } } diff --git a/web/public/locales/sk/objects.json b/web/public/locales/sk/objects.json index 2b3199df7..f0ea17df6 100644 --- a/web/public/locales/sk/objects.json +++ b/web/public/locales/sk/objects.json @@ -31,5 +31,23 @@ "handbag": "Kabelka", "tie": "Kravata", "suitcase": "Kufor", - "frisbee": "Frisbee" + "frisbee": "Frisbee", + "skis": "Lyže", + "snowboard": "Snowboard", + "sports_ball": "Športová lopta", + "kite": "Drak", + "baseball_bat": "Bejzbalová pálka", + "baseball_glove": "Baseballová rukavica", + "skateboard": "Skateboard", + "surfboard": "Surfová doska", + "tennis_racket": "Tenisová raketa", + "bottle": "Fľaša", + "plate": "Doska", + "wine_glass": "Pohár na víno", + "cup": "Pohár", + "fork": "Vidlička", + "knife": "Nôž", + "spoon": "Lyžica", + "bowl": "Misa", + "banana": "Banán" } diff --git a/web/public/locales/sk/views/configEditor.json b/web/public/locales/sk/views/configEditor.json index 7bfafd009..c10f789a8 100644 --- a/web/public/locales/sk/views/configEditor.json +++ b/web/public/locales/sk/views/configEditor.json @@ -12,5 +12,7 @@ "error": { "savingError": "Chyba ukladaní konfigurácie" } - } + }, + "safeConfigEditor": "Editor konfigurácie (núdzový režim)", + "safeModeDescription": "Frigate je v núdzovom režime kvôli chybe overenia konfigurácie." } diff --git a/web/public/locales/sk/views/explore.json b/web/public/locales/sk/views/explore.json index 5de31b69f..0fdd88411 100644 --- a/web/public/locales/sk/views/explore.json +++ b/web/public/locales/sk/views/explore.json @@ -48,6 +48,37 @@ "scrollViewTips": "Posúvaním zobrazíte významné momenty životného cyklu tohto objektu.", "autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.", "count": "{{first}} z {{second}}", - "trackedPoint": "Sledovaný bod" + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "visible": "Zistený {{label}}", + "entered_zone": "{{label}} vstúpil do {{zones}}", + "active": "{{label}} sa stal aktívnym", + "stationary": "{{label}} sa zastavil", + "attribute": { + "faceOrLicense_plate": "Pre {{label}} bol zistený {{attribute}}", + "other": "{{label}} rozpoznané ako {{attribute}}" + }, + "gone": "{{label}} zostalo", + "heard": "{{label}} počul", + "external": "Zistený {{label}}", + "header": { + "zones": "Zóny", + "ratio": "Pomer", + "area": "Oblasť" + } + }, + "annotationSettings": { + "title": "Nastavenia anotácií", + "showAllZones": { + "title": "Zobraziť všetky zóny", + "desc": "Vždy zobrazovať zóny na rámoch, do ktorých objekty vstúpili." + }, + "offset": { + "label": "Odsadenie anotácie", + "desc": "Tieto údaje pochádzajú z detekčného kanála vašej kamery, ale prekrývajú sa s obrázkami zo záznamového kanála. Je nepravdepodobné, že tieto dva streamy sú dokonale synchronizované. V dôsledku toho sa ohraničujúci rámček a zábery nebudú dokonale zarovnané. Na úpravu tohto posunu je však možné použiť pole annotation_offset.", + "documentation": "Prečítajte si dokumentáciu ", + "millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. Predvolené: 0" + } + } } } diff --git a/web/public/locales/sk/views/live.json b/web/public/locales/sk/views/live.json index ebb12d4cd..f16c2a8d6 100644 --- a/web/public/locales/sk/views/live.json +++ b/web/public/locales/sk/views/live.json @@ -43,7 +43,15 @@ "label": "Kliknite do rámčeka pre vycentrovanie PTZ kamery" } }, - "presets": "Predvoľby PTZ kamery" + "presets": "Predvoľby PTZ kamery", + "focus": { + "in": { + "label": "Zaostrenie PTZ kamery v" + }, + "out": { + "label": "Výstup zaostrenia PTZ kamery" + } + } }, "camera": { "enable": "Povoliť fotoaparát", @@ -72,5 +80,32 @@ "autotracking": { "enable": "Povoliť automatické sledovanie", "disable": "Zakázať automatické sledovanie" + }, + "transcription": { + "enable": "Povoliť živý prepis zvuku", + "disable": "Zakázať živý prepis zvuku" + }, + "streamStats": { + "enable": "Zobraziť štatistiky streamu", + "disable": "Skryť štatistiky streamu" + }, + "manualRecording": { + "title": "Nahrávanie na požiadanie", + "tips": "Spustiť manuálnu udalosť na základe nastavení uchovávania záznamu tejto kamery.", + "playInBackground": { + "label": "Hrať na pozadí", + "desc": "Povoľte túto možnosť, ak chcete pokračovať v streamovaní, aj keď je prehrávač skrytý." + }, + "showStats": { + "label": "Zobraziť štatistiky", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia", + "start": "Spustiť nahrávanie na požiadanie", + "started": "Spustené manuálne nahrávanie na požiadanie.", + "failedToStart": "Nepodarilo sa spustiť manuálne nahrávanie na požiadanie.", + "recordDisabledTips": "Keďže nahrávanie je v konfigurácii tejto kamery zakázané alebo obmedzené, uloží sa iba snímka.", + "end": "Ukončiť nahrávanie na požiadanie", + "ended": "Manuálne nahrávanie na požiadanie bolo ukončené." } } diff --git a/web/public/locales/sk/views/search.json b/web/public/locales/sk/views/search.json index cc567af26..a368ca123 100644 --- a/web/public/locales/sk/views/search.json +++ b/web/public/locales/sk/views/search.json @@ -41,6 +41,32 @@ "minSpeedMustBeLessOrEqualMaxSpeed": "Hodnota „min_speed“ musí byť menšia alebo rovná hodnote „max_speed“.", "maxSpeedMustBeGreaterOrEqualMinSpeed": "Hodnota „max_speed“ musí byť väčšia alebo rovná hodnote „min_speed“." } + }, + "tips": { + "title": "Ako používať textové filtre", + "desc": { + "text": "Filtre vám pomôžu zúžiť výsledky vyhľadávania. Tu je postup, ako ich použiť vo vstupnom poli:", + "step1": "Zadajte názov kľúča filtra, za ktorým nasleduje dvojbodka (napr. „kamery:“).", + "step2": "Vyberte hodnotu z návrhov alebo zadajte vlastnú.", + "step3": "Použite viacero filtrov tak, že ich pridáte jeden po druhom s medzerou medzi nimi.", + "step4": "Filtre dátumu (pred: a po:) používajú formát {{DateFormat}}.", + "step5": "Filter časového rozsahu používa formát {{exampleTime}}.", + "step6": "Filtre odstránite kliknutím na „x“ vedľa nich.", + "exampleLabel": "Príklad:" + } + }, + "header": { + "currentFilterType": "Hodnoty filtra", + "noFilters": "Filtre", + "activeFilters": "Aktívne filtre" } + }, + "similaritySearch": { + "title": "Vyhľadávanie podobností", + "active": "Vyhľadávanie podobnosti je aktívne", + "clear": "Jasné vyhľadávanie podobnosti" + }, + "placeholder": { + "search": "Hľadať…" } } diff --git a/web/public/locales/sk/views/settings.json b/web/public/locales/sk/views/settings.json index 27013c197..1944f2a1f 100644 --- a/web/public/locales/sk/views/settings.json +++ b/web/public/locales/sk/views/settings.json @@ -49,6 +49,45 @@ "title": "Uložené rozloženia", "desc": "Rozloženie kamier v skupine kamier je možné presúvať/zmeniť jeho veľkosť. Pozície sú uložené v lokálnom úložisku vášho prehliadača.", "clearAll": "Vymazať všetky rozloženia" + }, + "cameraGroupStreaming": { + "title": "Nastavenia streamovania skupiny kamier", + "desc": "Nastavenia streamovania pre každú skupinu kamier sú uložené v lokálnom úložisku vášho prehliadača.", + "clearAll": "Vymazať všetky nastavenia streamovania" + }, + "recordingsViewer": { + "title": "Prehliadač nahrávok", + "defaultPlaybackRate": { + "label": "Predvolená rýchlosť prehrávania", + "desc": "Predvolená rýchlosť prehrávania nahrávok." + } + }, + "calendar": { + "title": "Kalendár", + "firstWeekday": { + "label": "Prvý pracovný deň", + "desc": "Deň, kedy začínajú týždne v kalendári kontroly.", + "sunday": "Nedeľa", + "monday": "Pondelok" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Uložené rozloženie pre {{cameraName}} bolo vymazané", + "clearStreamingSettings": "Nastavenia streamovania pre všetky skupiny kamier boli vymazané." + }, + "error": { + "clearStoredLayoutFailed": "Nepodarilo sa vymazať uložené rozloženie: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nepodarilo sa vymazať nastavenia streamovania: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavenia obohatení", + "unsavedChanges": "Zmeny nastavení neuložených obohatení", + "birdClassification": { + "title": "Klasifikácia vtákov", + "desc": "Klasifikácia vtákov identifikuje známe vtáky pomocou kvantizovaného modelu Tensorflow. Keď je známy vták rozpoznaný, jeho bežný názov sa pridá ako podoznačenie (sub_label). Tieto informácie sú zahrnuté v používateľskom rozhraní, filtroch, ako aj v oznámeniach." } } } diff --git a/web/public/locales/sk/views/system.json b/web/public/locales/sk/views/system.json index ea3a3927e..c43e10e9a 100644 --- a/web/public/locales/sk/views/system.json +++ b/web/public/locales/sk/views/system.json @@ -52,9 +52,42 @@ "gpuDecoder": "GPU dekodér", "gpuInfo": { "vainfoOutput": { - "title": "Výstup Vainfo" + "title": "Výstup Vainfo", + "returnCode": "Návratový kód: {{code}}", + "processOutput": "Výstup procesu:", + "processError": "Chyba procesu:" + }, + "nvidiaSMIOutput": { + "title": "Výstup Nvidia SMI", + "name": "Meno: {{name}}", + "driver": "Vodič: {{driver}}", + "cudaComputerCapability": "Výpočtové možnosti CUDA: {{cuda_compute}}", + "vbios": "Informácie o VBiose: {{vbios}}" + }, + "closeInfo": { + "label": "Zatvorte informácie o GPU" + }, + "copyInfo": { + "label": "Kopírovať informácie o GPU" + }, + "toast": { + "success": "Informácie o grafickej karte boli skopírované do schránky" } - } + }, + "npuUsage": "Použitie NPU", + "npuMemory": "Pamäť NPU" + }, + "otherProcesses": { + "title": "Iné procesy", + "processCpuUsage": "Proces využitia CPU", + "processMemoryUsage": "Procesné využitie pamäte" + } + }, + "storage": { + "title": "Skladovanie", + "overview": "Prehľad", + "recordings": { + "title": "Nahrávky" } } } diff --git a/web/public/locales/sl/audio.json b/web/public/locales/sl/audio.json index 31562e8c9..bf5482cab 100644 --- a/web/public/locales/sl/audio.json +++ b/web/public/locales/sl/audio.json @@ -106,5 +106,39 @@ "piano": "Klavir", "electric_piano": "Digitalni klavir", "organ": "Orgle", - "electronic_organ": "Digitalne orgle" + "electronic_organ": "Digitalne orgle", + "chant": "Spev", + "mantra": "Mantra", + "child_singing": "Otroško petje", + "synthetic_singing": "Sintetično petje", + "humming": "Brenčanje", + "groan": "Stok", + "grunt": "Godrnjanje", + "wheeze": "Zadihan izdih", + "gasp": "Glasen Vzdih", + "pant": "Sopihanje", + "snort": "Smrkanje", + "throat_clearing": "Odkašljevanje", + "sneeze": "Kihanje", + "sniff": "Vohljaj", + "chewing": "Žvečenje", + "biting": "Grizenje", + "gargling": "Grgranje", + "stomach_rumble": "Grmotanje v Želodcu", + "heart_murmur": "Šum na Srcu", + "chatter": "Klepetanje", + "yip": "Jip", + "growling": "Rjovenje", + "whimper_dog": "Pasje Cviljenje", + "oink": "Oink", + "gobble": "Zvok Purana", + "wild_animals": "Divje Živali", + "roaring_cats": "Rjoveče Mačke", + "roar": "Rjovenje Živali", + "squawk": "Krik", + "patter": "Klepetanje", + "croak": "Kvakanje", + "rattle": "Ropotanje", + "whale_vocalization": "Kitova Vokalizacija", + "plucked_string_instrument": "Trgani Godalni Instrument" } diff --git a/web/public/locales/sl/common.json b/web/public/locales/sl/common.json index ff21c10ce..4468781f5 100644 --- a/web/public/locales/sl/common.json +++ b/web/public/locales/sl/common.json @@ -51,7 +51,40 @@ "h": "{{time}}h", "m": "{{time}}m", "s": "{{time}}s", - "yr": "le" + "yr": "le", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + } }, "menu": { "live": { @@ -67,9 +100,86 @@ }, "explore": "Brskanje", "theme": { - "nord": "Nord" + "nord": "Nord", + "label": "Teme", + "blue": "Modra", + "green": "Zelena", + "red": "Rdeča", + "highcontrast": "Visok Kontrast", + "default": "Privzeto" }, - "review": "Pregled" + "review": "Pregled", + "system": "Sistem", + "systemMetrics": "Sistemske metrike", + "configuration": "Konfiguracija", + "systemLogs": "Sistemski dnevniki", + "settings": "Nastavitve", + "configurationEditor": "Urejevalnik Konfiguracije", + "languages": "Jeziki", + "language": { + "en": "English (angleščina)", + "es": "Español (španščina)", + "zhCN": "简体中文 (poenostavljena kitajščina)", + "hi": "हिन्दी (hindijščina)", + "fr": "Français (francoščina)", + "ar": "العربية (arabščina)", + "pt": "Português (portugalščina)", + "ru": "Русский (ruščina)", + "de": "Deutsch (nemščina)", + "ja": "日本語 (japonščina)", + "tr": "Türkçe (turščina)", + "it": "Italiano (italijanščina)", + "nl": "Nederlands (nizozemščina)", + "sv": "Svenska (švedščina)", + "cs": "Čeština (češčina)", + "nb": "Norsk Bokmål (norveščina, bokmal)", + "ko": "한국어 (korejščina)", + "vi": "Tiếng Việt (vietnamščina)", + "fa": "فارسی (perzijščina)", + "pl": "Polski (poljščina)", + "uk": "Українська (ukrajinščina)", + "he": "עברית (hebrejščina)", + "el": "Ελληνικά (grščina)", + "ro": "Română (romunščina)", + "hu": "Magyar (madžarščina)", + "fi": "Suomi (finščina)", + "da": "Dansk (danščina)", + "sk": "Slovenčina (slovaščina)", + "yue": "粵語 (kantonščina)", + "th": "ไทย (tajščina)", + "sr": "Српски (srbščina)", + "sl": "Slovenščina (Slovenščina )", + "bg": "Български (bulgarščina)", + "withSystem": { + "label": "Uporabi sistemske nastavitve za jezik" + } + }, + "appearance": "Izgled", + "darkMode": { + "label": "Temni Način", + "light": "Svetlo", + "dark": "Temno", + "withSystem": { + "label": "Uporabi sistemske nastavitve za svetel ali temen način" + } + }, + "withSystem": "Sistem", + "help": "Pomoč", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Znova Zaženi Frigate", + "export": "Izvoz", + "faceLibrary": "Zbirka Obrazov", + "user": { + "title": "Uporabnik", + "account": "Račun", + "current": "Trenutni Uporabnik: {{user}}", + "anonymous": "anonimen", + "logout": "Odjava", + "setPassword": "Nastavi Geslo" + } }, "button": { "apply": "Uporabi", @@ -80,7 +190,7 @@ "back": "Nazaj", "pictureInPicture": "Slika v Sliki", "history": "Zgodovina", - "disabled": "Izklopljeno", + "disabled": "Onemogočeno", "copy": "Kopiraj", "exitFullscreen": "Izhod iz Celozaslonskega načina", "enabled": "Omogočen", @@ -88,7 +198,25 @@ "save": "Shrani", "saving": "Shranjevanje …", "cancel": "Prekliči", - "fullscreen": "Celozaslonski način" + "fullscreen": "Celozaslonski način", + "twoWayTalk": "Dvosmerni Pogovor", + "cameraAudio": "Zvok Kamere", + "on": "Vključen", + "off": "Izključen", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Izbriši", + "yes": "Da", + "no": "Ne", + "download": "Prenesi", + "info": "Info", + "suspended": "Začasno ustavljeno", + "unsuspended": "Obnovi", + "play": "Predvajaj", + "unselect": "Odznači", + "export": "Izvoz", + "deleteNow": "Izbriši Zdaj", + "next": "Naprej" }, "unit": { "speed": { @@ -105,7 +233,42 @@ }, "pagination": { "next": { - "label": "Pojdi na naslednjo stran" + "label": "Pojdi na naslednjo stran", + "title": "Naprej" + }, + "label": "paginacija", + "previous": { + "title": "Prejšnji", + "label": "Pojdi na prejšnjo stran" + }, + "more": "Več strani" + }, + "selectItem": "Izberi {{item}}", + "toast": { + "copyUrlToClipboard": "Povezava kopirana v odložišče.", + "save": { + "title": "Shrani", + "error": { + "title": "Napaka pri shranjevanju sprememb: {{errorMessage}}", + "noMessage": "Napaka pri shranjevanju sprememb konfiguracije" + } } - } + }, + "role": { + "title": "Vloga", + "admin": "Administrator", + "viewer": "Gledalec", + "desc": "Administratorji imajo poln dostop do vseh funkcij Frigate uporabniškega vmesnika. Gledalci so omejeni na gledanje kamer, zgodovine posnetkov in pregledovanje dogodkov." + }, + "accessDenied": { + "documentTitle": "Dostop zavrnjen - Frigate", + "title": "Dostop Zavrnjen", + "desc": "Nimate pravic za ogled te strani." + }, + "notFound": { + "documentTitle": "Ni Najdeno - Frigate", + "title": "404", + "desc": "Stran ni najdena" + }, + "readTheDocumentation": "Preberite dokumentacijo" } diff --git a/web/public/locales/sl/components/dialog.json b/web/public/locales/sl/components/dialog.json index e63f7c34b..46d1dfad8 100644 --- a/web/public/locales/sl/components/dialog.json +++ b/web/public/locales/sl/components/dialog.json @@ -12,11 +12,18 @@ "plus": { "review": { "question": { - "ask_full": "Ali je ta objekt {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Ali je ta objekt {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potrdi to oznako za Frigate Plus", + "ask_a": "Ali je ta objekt {{label}}?", + "ask_an": "Ali je ta objekt {{label}}?" }, "state": { "submitted": "Oddano" } + }, + "submitToPlus": { + "label": "Pošlji v Frigate+", + "desc": "Predmeti na lokacijah, ki se jim želite izogniti, niso lažni alarmi. Če jih označite kot lažne alarme, boste zmedli model." } }, "video": { @@ -28,7 +35,88 @@ "lastHour_one": "Zadnja ura", "lastHour_two": "Zadnji {{count}} uri", "lastHour_few": "Zadnje {{count}} ure", - "lastHour_other": "Zadnjih {{count}} ur" + "lastHour_other": "Zadnjih {{count}} ur", + "fromTimeline": "Izberi s Časovnice", + "custom": "Po meri", + "start": { + "title": "Začetni čas", + "label": "Izberi Začetni Čas" + }, + "end": { + "title": "Končni Čas", + "label": "Izberi Končni Čas" + } + }, + "name": { + "placeholder": "Poimenujte Izvoz" + }, + "select": "Izberi", + "export": "Izvoz", + "selectOrExport": "Izberi ali Izvozi", + "toast": { + "success": "Izvoz se je uspešno začel. Datoteko si oglejte v mapi /exports.", + "error": { + "failed": "Npaka pri začetku izvoza: {{error}}", + "endTimeMustAfterStartTime": "Končni čas mora biti po začetnem čase", + "noVaildTimeSelected": "Ni izbranega veljavnega časovnega obdobja" + } + }, + "fromTimeline": { + "saveExport": "Shrani Izvoz", + "previewExport": "Predogled Izvoza" } + }, + "streaming": { + "label": "Pretakanje", + "restreaming": { + "disabled": "Ponovno pretakanje za to kamero ni omogočeno.", + "desc": { + "title": "Za dodatne možnosti ogleda v živo in zvoka za to kamero nastavite go2rtc.", + "readTheDocumentation": "Preberi dokumentacijo" + } + }, + "showStats": { + "label": "Prikaži statistiko pretoka", + "desc": "Omogočite to možnost, če želite prikazati statistiko pretoka videa kamere." + }, + "debugView": "Pogled za Odpravljanje Napak" + }, + "search": { + "saveSearch": { + "label": "Varno Iskanje", + "desc": "Vnesite ime za to shranjeno iskanje.", + "placeholder": "Vnesite ime za iskanje", + "overwrite": "{{searchName}} že obstaja. Shranjevanje bo prepisalo obstoječo vrednost.", + "success": "Iskanje ({{searchName}}) je bilo shranjeno.", + "button": { + "save": { + "label": "Shrani to iskanje" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potrdi Brisanje", + "desc": { + "selected": "Ali ste prepričani, da želite izbrisati vse posnete videoposnetke, povezane s tem elementom pregleda?

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

Slednih objektov brez slike ni mogoče poslati v Frigate+." + } + }, + "cameras": { + "label": "Filtri Kamere", + "all": { + "title": "Vse Kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Prikaži Pregledano" + }, + "motion": { + "showMotionOnly": "Prikaži Samo Gibanje" + }, + "recognizedLicensePlates": { + "title": "Prepoznane Registrske Tablice", + "loadFailed": "Prepoznanih registrskih tablic ni bilo mogoče naložiti.", + "loading": "Nalaganje prepoznanih registrskih tablic…", + "placeholder": "Iskanje registrskih tablic…", + "noLicensePlatesFound": "Nobena registrska tablica ni bila najdena.", + "selectPlatesFromList": "Na seznamu izberite eno ali več registrskih tablic." } } diff --git a/web/public/locales/sl/views/configEditor.json b/web/public/locales/sl/views/configEditor.json index b8f76525d..5c69cc1b4 100644 --- a/web/public/locales/sl/views/configEditor.json +++ b/web/public/locales/sl/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "Napaka pri shranjevanju konfiguracije" } }, - "confirm": "Izhod brez shranjevanja?" + "confirm": "Izhod brez shranjevanja?", + "safeConfigEditor": "Urejevalnik konfiguracij (Varni Način)", + "safeModeDescription": "Frigate je v varnem načinu zaradi napake pri preverjanju konfiguracije." } diff --git a/web/public/locales/sl/views/explore.json b/web/public/locales/sl/views/explore.json index 97e7ca664..70fee301e 100644 --- a/web/public/locales/sl/views/explore.json +++ b/web/public/locales/sl/views/explore.json @@ -3,15 +3,28 @@ "title": "Funkcija razišči ni na voljo", "downloadingModels": { "setup": { - "visionModel": "Model vida" + "visionModel": "Model vida", + "visionModelFeatureExtractor": "Pridobivanje lastnosti modela vida", + "textModel": "Besedilni model", + "textTokenizer": "Tokenizator besedila" }, - "context": "Frigate prenaša potrebne modele vdelave za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti vaše omrežne povezave." + "context": "Frigate prenaša potrebne modele vdelave za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti vaše omrežne povezave.", + "tips": { + "context": "Morda boste želeli ponovno indeksirati vdelave (embeddings) svojih sledenih objektov, ko bodo modeli preneseni.", + "documentation": "Preberi dokumentacijo" + }, + "error": "Prišlo je do napake. Preverite dnevnike Frigate." }, "embeddingsReindexing": { "step": { "descriptionsEmbedded": "Vdelani opisi: ", - "trackedObjectsProcessed": "Obdelani sledeni predmeti: " - } + "trackedObjectsProcessed": "Obdelani sledeni predmeti: ", + "thumbnailsEmbedded": "Vdelane sličice: " + }, + "context": "Funkcija Explore se lahko uporablja, ko je ponovno indeksiranje vgraditev(embeddings) sledenih objektov končano.", + "startingUp": "Zagon…", + "estimatedTime": "Ocenjeni preostali čas:", + "finishingShortly": "Kmalu končano" } }, "documentTitle": "Razišči - Frigate", @@ -29,12 +42,61 @@ "estimatedSpeed": "Ocenjena hitrost", "description": { "placeholder": "Opis sledenega predmeta", - "label": "Opis" + "label": "Opis", + "aiTips": "Frigate od vašega ponudnika generativne UI ne bo zahteval opisa, dokler se življenjski cikel sledenega objekta ne konča." }, "recognizedLicensePlate": "Prepoznana registrska tablica", "objects": "Predmeti", "zones": "Območja", - "timestamp": "Časovni žig" + "timestamp": "Časovni žig", + "item": { + "button": { + "share": "Deli ta element mnenja", + "viewInExplore": "Poglej v Razišči Pogledu" + }, + "tips": { + "hasMissingObjects": "Prilagodite konfiguracijo, če želite, da Frigate shranjuje sledene objekte za naslednje oznake: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Od ponudnika {{provider}} je bil zahtevan nov opis. Glede na hitrost vašega ponudnika lahko regeneracija novega opisa traja nekaj časa.", + "updatedSublabel": "Podoznaka je bila uspešno posodobljena.", + "updatedLPR": "Registrska tablica je bila uspešno posodobljena.", + "audioTranscription": "Zahteva za zvočni prepis je bila uspešno izvedena." + }, + "error": { + "regenerate": "Klic ponudniku {{provider}} za nov opis ni uspel: {{errorMessage}}", + "updatedSublabelFailed": "Posodobitev podoznake ni uspela: {{errorMessage}}", + "updatedLPRFailed": "Posodobitev registrske tablice ni uspela: {{errorMessage}}", + "audioTranscription": "Zahteva za prepis zvoka ni uspela: {{errorMessage}}" + } + }, + "title": "Preglej Podrobnosti Elementa", + "desc": "Preglej podrobnosti elementa" + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi podoznako", + "desc": "Vnesite novo podoznako za {{label}}", + "descNoLabel": "Vnesite novo podoznako za ta sledeni objekt" + }, + "editLPR": { + "title": "Uredi registrsko tablico", + "desc": "Vnesite novo vrednost registrske tablice za {{label}}", + "descNoLabel": "Vnesite novo vrednost registrske tablice za ta sledeni objekt" + }, + "snapshotScore": { + "label": "Ocena Slike" + }, + "topScore": { + "label": "Najboljša Ocena", + "info": "Najboljša ocena je najvišji mediani rezultat za sledeni objekt, zato se lahko razlikuje od rezultata, prikazanega na sličici rezultata iskanja." + }, + "expandRegenerationMenu": "Razširi meni regeneracije", + "tips": { + "descriptionSaved": "Opis uspešno shranjen", + "saveDescriptionFailed": "Opisa ni bilo mogoče posodobiti: {{errorMessage}}" + } }, "itemMenu": { "findSimilar": { @@ -63,11 +125,85 @@ "downloadSnapshot": { "label": "Prenesi posnetek", "aria": "Prenesi posnetek" + }, + "addTrigger": { + "label": "Dodaj sprožilec", + "aria": "Dodaj sprožilec za ta sledeni objekt" + }, + "audioTranscription": { + "label": "Prepis", + "aria": "Zahtevajte prepis zvoka" } }, "dialog": { "confirmDelete": { "title": "Potrdi brisanje" } + }, + "trackedObjectDetails": "Podrobnosti Sledenega Objekta", + "type": { + "details": "podrobnosti", + "snapshot": "posnetek", + "video": "video", + "object_lifecycle": "življenjski cikel objekta" + }, + "objectLifecycle": { + "title": "Življenjski Cikel Objekta", + "noImageFound": "Za ta čas ni bila najdena nobena slika.", + "createObjectMask": "Ustvarite Masko Objekta", + "adjustAnnotationSettings": "Prilagodi nastavitve opomb", + "scrollViewTips": "Pomaknite se, da si ogledate pomembne trenutke življenjskega cikla tega predmeta.", + "count": "{{first}} od {{second}}", + "trackedPoint": "Sledena točka", + "lifecycleItemDesc": { + "visible": "{{label}} zaznan", + "entered_zone": "{{label}} je vstopil/a v {{zones}}", + "active": "{{label}} je postal aktiven", + "stationary": "{{label}} je postal nepremičen", + "attribute": { + "faceOrLicense_plate": "{{attribute}} je bil zaznan za {{label}}", + "other": "{{label}} zaznan kot {{attribute}}" + }, + "gone": "{{label}} levo", + "heard": "{{label}} slišano", + "external": "{{label}} zaznan", + "header": { + "zones": "Cone", + "ratio": "Razmerje", + "area": "Območje" + } + }, + "annotationSettings": { + "title": "Nastavitve Anotacij", + "showAllZones": { + "title": "Prikaži Vse Cone", + "desc": "Vedno prikaži območja na okvirjih, kjer so predmeti vstopili v območje." + }, + "offset": { + "label": "Anotacijski Odmik", + "documentation": "Preberi dokumentacijo ", + "millisecondsToOffset": "Odmik zaznanih anotacij v milisekundah. Privzeto: 0", + "tips": "NASVET: Predstavljajte si posnetek dogodka, v katerem oseba hodi od leve proti desni. Če je okvir dogodka na časovnici preveč levo od osebe, je treba vrednost zmanjšati. Podobno je treba vrednost povečati, če oseba hodi od leve proti desni in je okvir preveč pred njo.", + "toast": { + "success": "Odmik anotacij za {{camera}} je bil shranjen v konfiguracijsko datoteko. Znova zaženite Frigate, da uveljavite spremembe." + } + } + }, + "carousel": { + "previous": "Prejšnji diapozitiv", + "next": "Naslednji diapozitiv" + }, + "autoTrackingTips": "Položaji okvirjev bodo za kamere s samodejnim sledenjem netočni." + }, + "noTrackedObjects": "Ni Najdenih Sledenih Objektov", + "fetchingTrackedObjectsFailed": "Napaka pri pridobivanju sledenih objektov: {{errorMessage}}", + "searchResult": { + "tooltip": "Ujemanje {{type}} pri {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Sledeni objekt je bil uspešno izbrisan.", + "error": "Brisanje sledenega predmeta ni uspelo: {{errorMessage}}" + } + } } } diff --git a/web/public/locales/sl/views/faceLibrary.json b/web/public/locales/sl/views/faceLibrary.json index d59acc47e..9d5324759 100644 --- a/web/public/locales/sl/views/faceLibrary.json +++ b/web/public/locales/sl/views/faceLibrary.json @@ -7,16 +7,22 @@ "details": { "person": "Oseba", "unknown": "Nenznano", - "timestamp": "Časovni žig" + "timestamp": "Časovni žig", + "subLabelScore": "Ocena Podoznake", + "scoreInfo": "Rezultat podoznake je utežena ocena vseh stopenj gotovosti prepoznanih obrazov, zato se lahko razlikuje od ocene, prikazane na posnetku.", + "face": "Podrobnosti Obraza", + "faceDesc": "Podrobnosti sledenega objekta, ki je ustvaril ta obraz" }, "uploadFaceImage": { - "title": "Naloži nov obraz" + "title": "Naloži nov obraz", + "desc": "Naloži sliko za iskanje obrazov in vključitev v {{pageToggle}}" }, "deleteFaceAttempts": { "desc_one": "Ali ste prepričani, da želite izbrisati {{count}} obraz? Tega dejanja ni mogoče razveljaviti.", "desc_two": "Ali ste prepričani, da želite izbrisati {{count}} obraza? Tega dejanja ni mogoče razveljaviti.", "desc_few": "Ali ste prepričani, da želite izbrisati {{count}} obraze? Tega dejanja ni mogoče razveljaviti.", - "desc_other": "Ali ste prepričani, da želite izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti." + "desc_other": "Ali ste prepričani, da želite izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti.", + "title": "Izbriši Obraze" }, "toast": { "success": { @@ -27,8 +33,72 @@ "deletedName_one": "{{count}} je bil uspešno izbrisan.", "deletedName_two": "{{count}} obraza sta bila uspešno izbrisana.", "deletedName_few": "{{count}} obrazi so bili uspešno izbrisani.", - "deletedName_other": "{{count}} obrazov je bilo uspešno izbrisanih." + "deletedName_other": "{{count}} obrazov je bilo uspešno izbrisanih.", + "uploadedImage": "Slika je bila uspešno naložena.", + "addFaceLibrary": "Oseba {{name}} je bila uspešno dodana v Knjižnico Obrazov!", + "renamedFace": "Obraz uspešno preimenovan v {{name}}", + "trainedFace": "Uspešno treniran obraz.", + "updatedFaceScore": "Ocena obraza je bila uspešno posodobljena." + }, + "error": { + "uploadingImageFailed": "Nalaganje slike ni uspelo: {{errorMessage}}", + "addFaceLibraryFailed": "Neuspešno nastavljanje imena obraza: {{errorMessage}}", + "deleteFaceFailed": "Brisanje ni uspelo: {{errorMessage}}", + "deleteNameFailed": "Brisanje imena ni uspelo: {{errorMessage}}", + "renameFaceFailed": "Preimenovanje obraza ni uspelo: {{errorMessage}}", + "trainFailed": "Treniranje ni uspelo: {{errorMessage}}", + "updateFaceScoreFailed": "Posodobitev ocene obraza ni uspela: {{errorMessage}}" } }, - "documentTitle": "Knjižnica obrazov - Frigate" + "documentTitle": "Knjižnica obrazov - Frigate", + "collections": "Zbirke", + "createFaceLibrary": { + "title": "Ustvari Zbirko", + "desc": "Ustvari novo zbirko", + "new": "Ustvari Nov Obraz" + }, + "steps": { + "faceName": "Vnesi Ime Obraza", + "uploadFace": "Naloži Sliko Obraza", + "nextSteps": "Naslednji koraki", + "description": { + "uploadFace": "Naložite sliko osebe {{name}}, ki prikazuje obraz (slikan naravnost in ne iz kota). Slike ni treba obrezati samo na obraz." + } + }, + "train": { + "title": "Vlak", + "aria": "Izberite treniranje", + "empty": "Ni nedavnih poskusov prepoznavanja obrazov" + }, + "selectItem": "Izberi {{item}}", + "selectFace": "Izberi Obraz", + "deleteFaceLibrary": { + "title": "Izbriši Ime", + "desc": "Ali ste prepričani, da želite izbrisati zbirko {{name}}? S tem boste trajno izbrisali vse povezane obraze." + }, + "renameFace": { + "title": "Preimenuj Obraz", + "desc": "Vnesi novo ime za {{name}}" + }, + "button": { + "deleteFaceAttempts": "Izbriši Obraze", + "addFace": "Dodaj Obraz", + "renameFace": "Preimenuj Obraz", + "deleteFace": "Izbriši Obraz", + "uploadImage": "Naloži Sliko", + "reprocessFace": "Ponovna Obdelava Obraza" + }, + "imageEntry": { + "validation": { + "selectImage": "Izberite slikovno datoteko." + }, + "dropActive": "Sliko spustite tukaj…", + "dropInstructions": "Povlecite in spustite sliko sem ali kliknite za izbiro", + "maxSize": "Največja velikost: {{size}}MB" + }, + "nofaces": "Noben obraz ni na voljo", + "pixels": "{{area}}px", + "readTheDocs": "Preberi dokumentacijo", + "trainFaceAs": "Treniraj obraz kot:", + "trainFace": "Treniraj Obraz" } diff --git a/web/public/locales/sl/views/live.json b/web/public/locales/sl/views/live.json index 212137ba7..5b5261828 100644 --- a/web/public/locales/sl/views/live.json +++ b/web/public/locales/sl/views/live.json @@ -9,14 +9,163 @@ "ptz": { "move": { "clickMove": { - "disable": "Onemogoči funkcijo klikni in premakni" + "disable": "Onemogoči funkcijo klikni in premakni", + "label": "Kliknite v okvir, da postavite kamero na sredino", + "enable": "Omogoči premik s klikom" }, "left": { "label": "Premakni PTZ kamero v levo" }, "up": { "label": "Premakni PTZ kamero gor" + }, + "down": { + "label": "Premakni PTZ kamero navzdol" + }, + "right": { + "label": "Premakni PTZ kamero desno" } + }, + "zoom": { + "in": { + "label": "Povečaj PTZ kamero" + }, + "out": { + "label": "Pomanjšaj PTZ kamero" + } + }, + "focus": { + "in": { + "label": "Izostri PTZ kamero" + }, + "out": { + "label": "Razostri PTZ kamero" + } + }, + "frame": { + "center": { + "label": "Kliknite v okvir, da postavite PTZ kamero na sredino" + } + }, + "presets": "Prednastavitve PTZ kamere" + }, + "cameraAudio": { + "enable": "Omogoči Zvok Kamere", + "disable": "Onemogoči Zvok Kamere" + }, + "camera": { + "enable": "Omogoči Kamero", + "disable": "Onemogoči Kamero" + }, + "muteCameras": { + "enable": "Utišaj vse kamere", + "disable": "Vklopi Zvok Vsem Kameram" + }, + "detect": { + "enable": "Omogoči Detekcijo", + "disable": "Onemogoči Detekcijo" + }, + "recording": { + "enable": "Omogoči Snemanje", + "disable": "Onemogoči Snemanje" + }, + "snapshots": { + "enable": "Omogoči Slike", + "disable": "Onemogoči Slike" + }, + "audioDetect": { + "enable": "Omogoči Zvočno Detekcijo", + "disable": "Onemogoči Zvočno Detekcijo" + }, + "transcription": { + "enable": "Omogoči Prepisovanje Zvoka v Živo", + "disable": "Onemogoči Prepisovanje Zvoka v Živo" + }, + "autotracking": { + "enable": "Omogoči Samodejno Sledenje", + "disable": "Onemogoči Samodejno Sledenje" + }, + "streamStats": { + "enable": "Prikaži Statistiko Pretočnega Predvajanja", + "disable": "Skrij Statistiko Pretočnega Predvajanja" + }, + "manualRecording": { + "title": "Snemanje na Zahtevo", + "tips": "Začni ročni dogodek na podlagi nastavitev hranjenja posnetkov te kamere.", + "playInBackground": { + "label": "Predvajaj v ozadju", + "desc": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." + }, + "showStats": { + "label": "Prikaži Statistiko", + "desc": "Omogočite to možnost, če želite statistiko pretoka prikazati kot prekrivni sloj na viru kamere." + }, + "debugView": "Pogled za Odpravljanje Napak", + "start": "Začni snemanje na zahtevo", + "started": "Začelo se je ročno snemanje na zahtevo.", + "failedToStart": "Ročnega snemanja na zahtevo ni bilo mogoče začeti.", + "recordDisabledTips": "Ker je snemanje v nastavitvah te kamere onemogočeno ali omejeno, bo shranjena samo slika.", + "end": "Končaj snemanje na zahtevo", + "ended": "Ročno snemanje na zahtevo je končano.", + "failedToEnd": "Ročnega snemanja na zahtevo ni bilo mogoče končati." + }, + "streamingSettings": "Nastavitve Pretakanja", + "notifications": "Obvestila", + "audio": "Zvok", + "suspend": { + "forTime": "Začasno ustavi za: " + }, + "stream": { + "title": "Pretok", + "audio": { + "tips": { + "title": "Zvok mora biti predvajan iz vaše kamere in konfiguriran v go2rtc za ta pretok.", + "documentation": "Preberi Dokumentacijo " + }, + "available": "Za ta pretok je na voljo zvok", + "unavailable": "Zvok za ta pretok ni na voljo" + }, + "twoWayTalk": { + "tips": "Vaša naprava mora podpirati to funkcijo, WebRTC pa mora biti konfiguriran za dvosmerni pogovor.", + "tips.documentation": "Preberi dokumentacijo ", + "available": "Za ta tok je na voljo dvosmerni pogovor", + "unavailable": "Dvosmerni pogovor ni na voljo za ta pretok" + }, + "lowBandwidth": { + "tips": "Pogled v živo je v načinu nizke pasovne širine zaradi napak v nalaganju ali pretoku.", + "resetStream": "Ponastavi pretok" + }, + "playInBackground": { + "label": "Predvajaj v ozadju", + "tips": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." } + }, + "cameraSettings": { + "title": "{{camera}} Nastavitve", + "cameraEnabled": "Kamera Omogočena", + "objectDetection": "Zaznavanje Objektov", + "recording": "Snemanje", + "snapshots": "Slike", + "audioDetection": "Zvočna Detekcija", + "transcription": "Zvočni Prepis", + "autotracking": "Samodejno Sledenje" + }, + "history": { + "label": "Prikaži stare posnetke" + }, + "effectiveRetainMode": { + "modes": { + "all": "Vse", + "motion": "Gibanje", + "active_objects": "Aktivni Objekti" + }, + "notAllTips": "Vaša konfiguracija hranjenja posnetkov {{source}} je nastavljena na način : {{effectiveRetainMode}}, zato bo ta posnetek na zahtevo hranil samo segmente z {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Uredi Postavitev", + "group": { + "label": "Uredi Skupino Kamere" + }, + "exitEdit": "Izhod iz Urejanja" } } diff --git a/web/public/locales/sl/views/settings.json b/web/public/locales/sl/views/settings.json index af8f70748..321f35ce2 100644 --- a/web/public/locales/sl/views/settings.json +++ b/web/public/locales/sl/views/settings.json @@ -18,7 +18,8 @@ "debug": "Razhroščevanje", "users": "Uporabniki", "notifications": "Obvestila", - "frigateplus": "Frigate+" + "frigateplus": "Frigate+", + "motionTuner": "Nastavitev Gibanja" }, "masksAndZones": { "zones": { @@ -59,12 +60,146 @@ "desc": "Samodejno preklopite na pogled kamere v živo, ko je zaznana aktivnost. Če onemogočite to možnost, se statične slike kamere na nadzorni plošči v živo posodobijo le enkrat na minuto." }, "playAlertVideos": { - "label": "Predvajajte opozorilne videoposnetke" + "label": "Predvajajte opozorilne videoposnetke", + "desc": "Privzeto se nedavna opozorila na nadzorni plošči predvajajo kot kratki ponavljajoči videoposnetki . To možnost onemogočite, če želite, da se v tej napravi/brskalniku prikaže samo statična slika nedavnih opozoril." } }, "storedLayouts": { "title": "Sharnjene Postavitve", - "desc": "Postaviteve kamer v skupini kamer je mogoče povleči/prilagoditi. Položaji so shranjeni v lokalnem pomnilniku vašega brskalnika." + "desc": "Postaviteve kamer v skupini kamer je mogoče povleči/prilagoditi. Položaji so shranjeni v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Postavitve" + }, + "cameraGroupStreaming": { + "title": "Nastavitve Pretakanja Skupine Kamer", + "desc": "Nastavitve pretakanja za vsako skupino kamer so shranjene v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Nastavitve Pretakanja" + }, + "recordingsViewer": { + "title": "Pregledovalnik Posnetkov", + "defaultPlaybackRate": { + "label": "Privzeta Hitrost Predvajanja", + "desc": "Privzeta Hitrost Predvajanja za Shranjene Posnetke." + } + }, + "calendar": { + "title": "Koledar", + "firstWeekday": { + "label": "Prvi Delovni Dan", + "desc": "Dan, na katerega se začnejo tedni v koledarju za preglede.", + "sunday": "Nedelja", + "monday": "Ponedeljek" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Shranjena postavitev za {{cameraName}} je bila izbrisana", + "clearStreamingSettings": "Nastavitve pretakanja za vse skupine kamer so bile izbrisane." + }, + "error": { + "clearStoredLayoutFailed": "Shranjene postavitve ni bilo mogoče izbrisati: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nastavitev pretakanja ni bilo mogoče izbrisati: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavitve Obogatitev", + "unsavedChanges": "Neshranjene Spremembe Nastavitev Obogatitev", + "birdClassification": { + "title": "Klasifikacija ptic", + "desc": "Klasifikacija ptic identificira znane ptice z uporabo kvantiziranega Tensorflow modela. Ko je znana ptica prepoznana, se njeno splošno ime doda kot podoznaka. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila." + }, + "semanticSearch": { + "title": "Semantično Iskanje", + "desc": "Semantično iskanje v Frigate vam omogoča iskanje sledenih objektov znotraj vaših pregledov, pri čemer lahko uporabite izvorno sliko, uporabniško določen besedilni opis ali samodejno ustvarjen opis.", + "readTheDocumentation": "Preberi Dokumentacijo", + "reindexNow": { + "label": "Ponovno Indeksiraj Zdaj", + "desc": "Ponovno indeksiranje bo regeneriralo vdelave (embeddings) za vse sledene objekte. Ta postopek se izvaja v ozadju in lahko zelo obremeni vaš procesor ter traja precej časa, odvisno od števila sledenih objektov, ki jih imate.", + "confirmTitle": "Potrdi Ponovno Indeksiranje", + "confirmDesc": "Ali ste prepričani, da želite ponovno indeksirati vse vdelave (embeddings) sledenih objektov? Ta postopek se bo izvajal v ozadju, vendar lahko zelo obremeni vaš procesor in traja kar nekaj časa. Napredek si lahko ogledate na strani Razišči.", + "confirmButton": "Ponovno Indeksiranje", + "success": "Ponovno indeksiranje se je uspešno začelo.", + "alreadyInProgress": "Ponovno indeksiranje je že v teku.", + "error": "Ponovnega indeksiranja ni bilo mogoče začeti: {{errorMessage}}" + }, + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za vdelave (embeddings) semantičnih iskanj.", + "small": { + "title": "majhen", + "desc": "Uporaba načina small uporablja kvantizirano različico modela, ki porabi manj RAM-a in deluje hitreje na procesorju z zelo zanemarljivo razliko v kakovosti vdelave (embedding)." + }, + "large": { + "title": "velik", + "desc": "Uporaba možnosti large uporablja celoten model Jina in se bo, če je mogoče, samodejno izvajal na grafičnem procesorju." + } + } + }, + "faceRecognition": { + "title": "Prepoznavanje Obrazov", + "desc": "Prepoznavanje obrazov omogoča, da se ljudem dodelijo imena, in ko Frigate prepozna njihov obraz, se detekciji dodeli ime kot podoznako. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila.", + "readTheDocumentation": "Preberi Dokumentacijo", + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za prepoznavanje obrazov.", + "small": { + "title": "majhen", + "desc": "Uporaba small uporablja model vdelave (embedding) obrazov FaceNet, ki učinkovito deluje na večini procesorjev." + }, + "large": { + "title": "velik", + "desc": "Uporaba large uporablja model vdelave (embedding) obrazov ArcFace in se bo samodejno zagnala na grafičnem procesorju, če bo to mogoče." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznavanje Registrskih Tablic", + "desc": "Frigate lahko prepozna registrske tablice na vozilih in samodejno doda zaznane znake v polje recognized_license_plate ali znano ime kot podoznako objektom tipa car. Pogost primer uporabe je lahko branje registrskih tablic avtomobilov, ki se ustavijo na dovozu, ali avtomobilov, ki se peljejo mimo po ulici.", + "readTheDocumentation": "Preberi Dokumentacijo" + }, + "restart_required": "Potreben je ponovni zagon (Nastavitve Obogatitve so bile spremenjene)", + "toast": { + "success": "Nastavitve Obogatitev so shranjene. Znova zaženite Frigate, da uveljavite spremembe.", + "error": "Shranjevanje sprememb konfiguracije ni uspelo: {{errorMessage}}" + } + }, + "camera": { + "title": "Nastavitve Kamere", + "streams": { + "title": "Pretoki" + }, + "object_descriptions": { + "title": "Opisi objektov z uporabo generativne UI", + "desc": "Začasno omogoči/onemogoči opise objektov z uporabo generativne UI za to kamero. Ko so onemogočeni, opisi, ki jih ustvari UI, ne bodo zahtevani za sledene objekte na tej kameri." + }, + "review": { + "title": "Pregled", + "desc": "Začasno omogoči/onemogoči opozorila in zaznavanja za to kamero, dokler se Frigate ne zažene znova. Ko je onemogočeno, ne bodo ustvarjeni novi elementi pregleda. ", + "alerts": "Opozorila ", + "detections": "Detekcije " + }, + "reviewClassification": { + "title": "Pregled Klasifikacij", + "readTheDocumentation": "Preberi Dokumentacijo", + "noDefinedZones": "Za to kamero ni določenih nobenih con.", + "objectAlertsTips": "Vsi objekti {{alertsLabels}} na {{cameraName}} bodo prikazani kot Opozorila.", + "unsavedChanges": "Neshranjene nastavitve Pregleda Klasifikacije za {{camera}}", + "selectAlertsZones": "Izberite cone za Opozorila", + "selectDetectionsZones": "Izberite cone za Zaznavanje", + "limitDetections": "Omejite zaznavanje na določene cone" + }, + "addCamera": "Dodaj Novo Kamero", + "editCamera": "Uredi Kamero:", + "selectCamera": "Izberi Kamero", + "backToSettings": "Nazaj na Nastavitve Kamere", + "cameraConfig": { + "add": "Dodaj Kamero", + "edit": "Uredi Kamero", + "description": "Konfigurirajte nastavitve kamere, vključno z pretočnimi vhodi in vlogami.", + "name": "Ime Kamere", + "nameRequired": "Ime kamere je obvezno", + "nameInvalid": "Ime kamere mora vsebovati samo črke, številke, podčrtaje ali vezaje", + "namePlaceholder": "npr. vhodna_vrata" } } } diff --git a/web/public/locales/sl/views/system.json b/web/public/locales/sl/views/system.json index 4a19721c0..6562321a2 100644 --- a/web/public/locales/sl/views/system.json +++ b/web/public/locales/sl/views/system.json @@ -7,7 +7,8 @@ "frigate": "Frigate dnevniki - Frigate", "go2rtc": "Go2RTC dnevniki - Frigate", "nginx": "Nginx dnevniki - Frigate" - } + }, + "enrichments": "Statistika Obogatitev - Frigate" }, "logs": { "download": { @@ -23,6 +24,13 @@ "timestamp": "Časovni žig", "message": "Sporočilo", "tag": "Oznaka" + }, + "tips": "Dnevniki se pretakajo s strežnika", + "toast": { + "error": { + "fetchingLogsFailed": "Napaka pri pridobivanju dnevnikov: {{errorMessage}}", + "whileStreamingLogs": "Napaka med pretakanjem dnevnikov: {{errorMessage}}" + } } }, "storage": { @@ -100,7 +108,67 @@ "title": "Kamere", "overview": "Pregled", "info": { - "aspectRatio": "razmerje stranic" + "aspectRatio": "razmerje stranic", + "cameraProbeInfo": "{{camera}} Podrobne Informacije Kamere", + "streamDataFromFFPROBE": "Podatki o pretoku se pridobijo z ukazom ffprobe.", + "fetching": "Pridobivanje Podatkov Kamere", + "stream": "Pretok {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Ločljivost:", + "fps": "FPS:", + "unknown": "Neznano", + "audio": "Zvok:", + "error": "Napaka: {{error}}", + "tips": { + "title": "Podrobne Informacije Kamere" + } + }, + "framesAndDetections": "Okvirji / Zaznave", + "label": { + "camera": "kamera", + "detect": "zaznaj", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "zajemanje", + "overallFramesPerSecond": "skupno število sličic na sekundo (FPS)", + "overallDetectionsPerSecond": "skupno število zaznav na sekundo", + "overallSkippedDetectionsPerSecond": "skupno število preskočenih zaznav na sekundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} zajem", + "cameraDetect": "{{camName}} zaznavanje", + "cameraFramesPerSecond": "{{camName}} sličic na sekundo (FPS)", + "cameraDetectionsPerSecond": "{{camName}} detekcij na sekundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih zaznav na sekundo" + }, + "toast": { + "success": { + "copyToClipboard": "Podatki sonde so bili kopirani v odložišče." + }, + "error": { + "unableToProbeCamera": "Ni mogoče preveriti podrobnosti kamere: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Zadnja osvežitev: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoko porabo procesorja FFmpeg ({{ffmpegAvg}} %)", + "detectHighCpuUsage": "{{camera}} ima visoko porabo procesorja za zaznavanje ({{detectAvg}} %)", + "healthy": "Sistem je zdrav", + "reindexingEmbeddings": "Ponovno indeksiranje vdelanih elementov (embeddings) ({{processed}}% končano)", + "cameraIsOffline": "{{camera}} je nedosegljiva", + "detectIsSlow": "{{detect}} je počasen ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je zelo počasen ({{speed}} ms)" + }, + "enrichments": { + "title": "Obogatitve", + "infPerSecond": "Inference Na Sekundo", + "embeddings": { + "face_recognition": "Prepoznavanje Obrazov", + "plate_recognition": "Prepoznavanje Registrskih Tablic", + "face_recognition_speed": "Hitrost Prepoznavanja Obrazov", + "plate_recognition_speed": "Hitrost Prepoznavanja Registrskih Tablic", + "yolov9_plate_detection": "YOLOv9 Zaznavanje Registrskih Tablic" } } } diff --git a/web/public/locales/sr/audio.json b/web/public/locales/sr/audio.json index a9e52ade6..63c1c25f0 100644 --- a/web/public/locales/sr/audio.json +++ b/web/public/locales/sr/audio.json @@ -11,5 +11,7 @@ "whispering": "Šaptanje", "bus": "Autobus", "laughter": "Smeh", - "train": "Voz" + "train": "Voz", + "boat": "Brod", + "crying": "Plač" } diff --git a/web/public/locales/sr/common.json b/web/public/locales/sr/common.json index a68b33248..06557f2ec 100644 --- a/web/public/locales/sr/common.json +++ b/web/public/locales/sr/common.json @@ -27,5 +27,6 @@ "year_few": "2,3,4,22,23,24,32,33,34,42,...", "year_other": "", "mo": "{{time}}mes" - } + }, + "readTheDocumentation": "Прочитајте документацију" } diff --git a/web/public/locales/sr/components/auth.json b/web/public/locales/sr/components/auth.json index f601ec61a..ecaa132ac 100644 --- a/web/public/locales/sr/components/auth.json +++ b/web/public/locales/sr/components/auth.json @@ -7,7 +7,9 @@ "usernameRequired": "Korisničko ime je obavezno", "passwordRequired": "Lozinka je obavezna", "rateLimit": "Prekoračeno ograničenje brzine. Pokušajte ponovo kasnije.", - "loginFailed": "Prijava nije uspela" + "loginFailed": "Prijava nije uspela", + "unknownError": "Nepoznata greška. Proveri logove.", + "webUnknownError": "Nepoznata greška. Proveri logove u konzoli." } } } diff --git a/web/public/locales/sr/components/camera.json b/web/public/locales/sr/components/camera.json index 6be8272ec..1bb6c3020 100644 --- a/web/public/locales/sr/components/camera.json +++ b/web/public/locales/sr/components/camera.json @@ -11,7 +11,11 @@ } }, "name": { - "label": "Ime" + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Naziv grupe kamera mora imati bar 2 karaktera." + } } } } diff --git a/web/public/locales/sr/components/dialog.json b/web/public/locales/sr/components/dialog.json index 8c5a7c1c4..ead50e869 100644 --- a/web/public/locales/sr/components/dialog.json +++ b/web/public/locales/sr/components/dialog.json @@ -13,6 +13,11 @@ "submitToPlus": { "label": "Pošalji na Frigate+", "desc": "Objekti na lokacijama koje želite da izbegnete nisu lažno pozitivni. Slanje lažno pozitivnih rezultata će zbuniti model." + }, + "review": { + "question": { + "ask_a": "Da li je ovaj objekat {{label}}?" + } } } } diff --git a/web/public/locales/sr/components/filter.json b/web/public/locales/sr/components/filter.json index e00ac754d..d7b8323f6 100644 --- a/web/public/locales/sr/components/filter.json +++ b/web/public/locales/sr/components/filter.json @@ -10,6 +10,10 @@ "count_other": "{{count}} Oznake" }, "zones": { - "label": "Zone" + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } } } diff --git a/web/public/locales/sr/objects.json b/web/public/locales/sr/objects.json index 75f353ded..4edf4728b 100644 --- a/web/public/locales/sr/objects.json +++ b/web/public/locales/sr/objects.json @@ -5,5 +5,6 @@ "motorcycle": "Motor", "airplane": "Avion", "bus": "Autobus", - "train": "Voz" + "train": "Voz", + "boat": "Brod" } diff --git a/web/public/locales/sr/views/events.json b/web/public/locales/sr/views/events.json index 8a1b76e45..4097e5666 100644 --- a/web/public/locales/sr/views/events.json +++ b/web/public/locales/sr/views/events.json @@ -8,6 +8,7 @@ "allCameras": "Sve Kamere", "empty": { "alert": "Nema upozorenja za pregled", - "detection": "Nema detekcija za pregled" + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu" } } diff --git a/web/public/locales/sr/views/exports.json b/web/public/locales/sr/views/exports.json index a12e06163..ff71c75d5 100644 --- a/web/public/locales/sr/views/exports.json +++ b/web/public/locales/sr/views/exports.json @@ -6,6 +6,7 @@ "deleteExport.desc": "Da li zaista želite obrisati {{exportName}}?", "editExport": { "title": "Preimenuj izvoz", - "desc": "Unesite novo ime za ovaj izvoz." + "desc": "Unesite novo ime za ovaj izvoz.", + "saveExport": "Sačuvaj izvoz" } } diff --git a/web/public/locales/sr/views/faceLibrary.json b/web/public/locales/sr/views/faceLibrary.json index 766a52aa9..c2aa8367b 100644 --- a/web/public/locales/sr/views/faceLibrary.json +++ b/web/public/locales/sr/views/faceLibrary.json @@ -7,6 +7,8 @@ "details": { "person": "Osoba", "subLabelScore": "Sub Label Skor", - "scoreInfo": "Rezultat podoznake je otežan rezultat za sve prepoznate pouzdanosti lica, tako da se može razlikovati od rezultata prikazanog na snimku." + "scoreInfo": "Rezultat podoznake je otežan rezultat za sve prepoznate pouzdanosti lica, tako da se može razlikovati od rezultata prikazanog na snimku.", + "face": "Detalji lica", + "faceDesc": "Detalji praćenog objekta koji je generisao ovo lice" } } diff --git a/web/public/locales/sr/views/live.json b/web/public/locales/sr/views/live.json index fe19046a3..1374fe163 100644 --- a/web/public/locales/sr/views/live.json +++ b/web/public/locales/sr/views/live.json @@ -7,6 +7,14 @@ "disable": "Onemogućite dvosmerni razgovor" }, "cameraAudio": { - "enable": "Omogući zvuk kamere" + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite na sliku da bi centrirali kameru" + } + } } } diff --git a/web/public/locales/sr/views/search.json b/web/public/locales/sr/views/search.json index 3ab007f60..d72036c66 100644 --- a/web/public/locales/sr/views/search.json +++ b/web/public/locales/sr/views/search.json @@ -5,6 +5,8 @@ "button": { "clear": "Obriši pretragu", "save": "Sačuvaj pretragu", - "delete": "Izbrišite sačuvanu pretragu" + "delete": "Izbrišite sačuvanu pretragu", + "filterInformation": "Filtriraj informacije", + "filterActive": "Aktivni filteri" } } diff --git a/web/public/locales/sr/views/settings.json b/web/public/locales/sr/views/settings.json index 07a4ea59d..2957af0f2 100644 --- a/web/public/locales/sr/views/settings.json +++ b/web/public/locales/sr/views/settings.json @@ -5,6 +5,7 @@ "camera": "Podešavanje kamera - Frigate", "enrichments": "Podešavanja obogaćivanja - Frigate", "masksAndZones": "Uređivač maski i zona - Frigate", - "motionTuner": "Tjuner pokreta - Frigate" + "motionTuner": "Tjuner pokreta - Frigate", + "general": "Generalna podešavanja - Frigate" } } diff --git a/web/public/locales/sr/views/system.json b/web/public/locales/sr/views/system.json index 07f260401..5cd6faa23 100644 --- a/web/public/locales/sr/views/system.json +++ b/web/public/locales/sr/views/system.json @@ -6,7 +6,9 @@ "enrichments": "Statistika obogaćivanja - Frigate", "logs": { "frigate": "Frigate logovi - Frigate", - "go2rtc": "Go2RTC dnevnici - Frigate" + "go2rtc": "Go2RTC dnevnici - Frigate", + "nginx": "Nginx logovi - Frigate" } - } + }, + "title": "Sistem" } diff --git a/web/public/locales/sv/audio.json b/web/public/locales/sv/audio.json index 2e685096c..f66923922 100644 --- a/web/public/locales/sv/audio.json +++ b/web/public/locales/sv/audio.json @@ -3,7 +3,7 @@ "bicycle": "Cykel", "speech": "Tal", "car": "Bil", - "bellow": "Under", + "bellow": "Vrål", "motorcycle": "Motorcykel", "whispering": "Viskning", "bus": "Buss", diff --git a/web/public/locales/sv/common.json b/web/public/locales/sv/common.json index a220db585..842ff4604 100644 --- a/web/public/locales/sv/common.json +++ b/web/public/locales/sv/common.json @@ -253,5 +253,6 @@ "meters": "meter" } }, - "selectItem": "Välj {{item}}" + "selectItem": "Välj {{item}}", + "readTheDocumentation": "Läs dokumentationen" } diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json index 42af6ea41..e41779b17 100644 --- a/web/public/locales/sv/components/dialog.json +++ b/web/public/locales/sv/components/dialog.json @@ -3,21 +3,26 @@ "button": "Starta om", "restarting": { "title": "Frigate startar om", - "content": "Sidan uppdateras om {{countdown}} seconds.", - "button": "Tvinga uppdatering nu" + "content": "Sidan uppdateras om {{countdown}} sekunder.", + "button": "Tvinga omladdning nu" }, "title": "Är du säker på att du vill starta om Frigate?" }, "explore": { "plus": { "submitToPlus": { - "label": "Skicka till Frigate+" + "label": "Skicka till Frigate+", + "desc": "Objekt på platser du vill undvika är inte falska positiva resultat. Att skicka in dem som falska positiva resultat kommer att förvirra modellen." }, "review": { "question": { "ask_a": "Är detta objektet {{label}}?", "ask_an": "Är detta objektet en {{label}}?", - "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?" + "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Bekräfta denna etikett för Frigate Plus" + }, + "state": { + "submitted": "Inskickad" } } }, @@ -37,12 +42,79 @@ "end": { "title": "Slut Tid", "label": "Välj Sluttid" - } + }, + "custom": "Anpassad" }, "name": { "placeholder": "Ge exporten ett namn" }, "select": "Välj", - "export": "Eksport" + "export": "Eksport", + "selectOrExport": "Välj eller exportera", + "toast": { + "success": "Exporten har startats. Visa filen i mappen /exports.", + "error": { + "failed": "Misslyckades med att starta exporten: {{error}}", + "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", + "noVaildTimeSelected": "Inget giltigt tidsintervall valt" + } + }, + "fromTimeline": { + "saveExport": "Spara export", + "previewExport": "Förhandsgranska export" + } + }, + "streaming": { + "label": "Videoström", + "restreaming": { + "disabled": "Omströmning är inte aktiverad för den här kameran.", + "desc": { + "title": "Konfigurera go2rtc för ytterligare livevisningsalternativ och ljud för den här kameran.", + "readTheDocumentation": "Läs dokumentationen" + } + }, + "showStats": { + "label": "Visa strömstatistik", + "desc": "Aktivera det här alternativet för att visa strömstatistik som ett överlägg över kameraflödet." + }, + "debugView": "Felsöknings vy" + }, + "search": { + "saveSearch": { + "overwrite": "{{searchName}} finns redan. Om du sparar skrivs det befintliga värdet över.", + "success": "Sökningen ({{searchName}}) har sparats.", + "button": { + "save": { + "label": "Spara den här sökningen" + } + }, + "label": "Spara Sökning", + "desc": "Ange ett namn för den här sparade sökningen.", + "placeholder": "Ange ett namn för din sökning" + } + }, + "recording": { + "confirmDelete": { + "title": "Bekräfta radering", + "desc": { + "selected": "Är du säker på att du vill radera all inspelad video som är kopplad till det här granskningsobjektet?

Håll ner Shift-tangenten för att hoppa över den här dialogrutan i framtiden." + }, + "toast": { + "success": "Videoklipp som är kopplade till de valda granskningsobjekten har raderats.", + "error": "Misslyckades med att ta bort: {{error}}" + } + }, + "button": { + "export": "Exportera", + "markAsReviewed": "Markera som granskad", + "deleteNow": "Ta bort nu" + } + }, + "imagePicker": { + "selectImage": "Välj miniatyrbilden för ett spårat objekt", + "search": { + "placeholder": "Sök efter etikett eller underetikett..." + }, + "noImages": "Inga miniatyrbilder hittades för den här kameran" } } diff --git a/web/public/locales/sv/components/filter.json b/web/public/locales/sv/components/filter.json index c29110cc7..5d1bf50c6 100644 --- a/web/public/locales/sv/components/filter.json +++ b/web/public/locales/sv/components/filter.json @@ -14,12 +14,17 @@ "label": "Zoner", "all": { "title": "Alla zoner", - "short": "Soner" + "short": "Zoner" } }, "features": { "hasSnapshot": "Har ögonblicksbild", - "hasVideoClip": "Har ett video klipp" + "hasVideoClip": "Har ett video klipp", + "submittedToFrigatePlus": { + "label": "Skickat till Frigate+", + "tips": "Du måste först filtrera på spårade objekt som har en ögonblicksbild.

Spårade objekt utan ögonblicksbild kan inte skickas till Frigate+." + }, + "label": "Detaljer" }, "sort": { "dateAsc": "Datum (Stigande)", @@ -42,12 +47,22 @@ "settings": { "title": "Inställningar", "defaultView": { - "title": "Standard Vy" + "title": "Standard Vy", + "summary": "Sammanfattning", + "desc": "När inga filter är valda, visa en översikt av de senaste spårade objekten per etikett-typ eller visa ett ofiltrerat rutnät.", + "unfilteredGrid": "Ofiltrerat Rutnät" }, "searchSource": { "options": { - "description": "Beskrivning" - } + "description": "Beskrivning", + "thumbnailImage": "Miniatyrbild" + }, + "label": "Sökkälla", + "desc": "Välj om du vill söka miniatyrbilderna eller beskrivningarna av de spårade objekten." + }, + "gridColumns": { + "desc": "Välj antal kolumner i rutnätsvy.", + "title": "Kolumner i Rutnät" } }, "date": { @@ -67,11 +82,16 @@ "all": { "short": "Datum", "title": "Alla datum" - } + }, + "selectPreset": "Välj Förval…" }, "recognizedLicensePlates": { "noLicensePlatesFound": "Inga registreringsplåtar hittade.", - "selectPlatesFromList": "Välj en eller flera registreringsplåtar från listan." + "selectPlatesFromList": "Välj en eller flera registreringsplåtar från listan.", + "title": "Igenkända Registreringsskyltar", + "loadFailed": "Misslyckades med att ladda igenkända registreringsskyltar.", + "placeholder": "Skriv för att söka registreringsskyltar…", + "loading": "Laddar igenkända registreringsskyltar…" }, "more": "Flera filter", "reset": { @@ -81,5 +101,35 @@ "label": "Under kategori", "all": "Alla under kategorier" }, - "estimatedSpeed": "Estimerad hastighet ({{unit}})" + "estimatedSpeed": "Estimerad hastighet ({{unit}})", + "classes": { + "all": { + "title": "Alla Klasser" + }, + "count_one": "{{count}} Klass", + "count_other": "{{count}} Klasser", + "label": "Klasser" + }, + "timeRange": "Tidsspann", + "logSettings": { + "loading": { + "title": "Laddar", + "desc": "När loggvyn är rullad till slutet, strömmas automatiskt nya loggar till vyn." + }, + "filterBySeverity": "Filtrera logg på allvarlighetsgrad", + "disableLogStreaming": "Inaktivera strömning av logg", + "allLogs": "Alla loggar", + "label": "Filter loggnivå" + }, + "trackedObjectDelete": { + "title": "Bekräfta Borttagning", + "toast": { + "success": "Spårade objekt borttagna.", + "error": "Misslyckades med att ta bort spårade objekt: {{errorMessage}}" + }, + "desc": "Borttagning av dessa {{objectLength}} spårade objekt tar bort ögonblicksbild, sparade inbäddningar, och tillhörande livscykelposter. Inspelat material av dessa spårade objekt i Historievyn kommer INTE att tas bort.

Vill du verkligen fortsätta?

Håll ner Skift-tangenten för att hoppa över denna dialog i framtiden." + }, + "zoneMask": { + "filterBy": "Filtrera på zonmaskering" + } } diff --git a/web/public/locales/sv/components/icons.json b/web/public/locales/sv/components/icons.json index e15428582..afdcfb7d9 100644 --- a/web/public/locales/sv/components/icons.json +++ b/web/public/locales/sv/components/icons.json @@ -1,7 +1,7 @@ { "iconPicker": { "search": { - "placeholder": "Sök efter ikon…" + "placeholder": "Sök efter en ikon…" }, "selectIcon": "Välj en ikon" } diff --git a/web/public/locales/sv/components/player.json b/web/public/locales/sv/components/player.json index b41c5dd65..24f1b1e96 100644 --- a/web/public/locales/sv/components/player.json +++ b/web/public/locales/sv/components/player.json @@ -39,7 +39,7 @@ "decodedFrames": "Avkodade bildrutor:", "droppedFrameRate": "Frekvens för bortfallna bildrutor:" }, - "cameraDisabled": "Kameran är disablead", + "cameraDisabled": "Kameran är inaktiverad", "toast": { "error": { "submitFrigatePlusFailed": "Bildruta har skickats till Frigate+ med misslyckat resultat" diff --git a/web/public/locales/sv/views/configEditor.json b/web/public/locales/sv/views/configEditor.json index 27409c968..7b96ff9fe 100644 --- a/web/public/locales/sv/views/configEditor.json +++ b/web/public/locales/sv/views/configEditor.json @@ -12,5 +12,7 @@ }, "documentTitle": "Ändra konfiguration - Frigate", "configEditor": "Ändra konfiguration", - "confirm": "Avsluta utan att spara?" + "confirm": "Avsluta utan att spara?", + "safeConfigEditor": "Konfigurationsredigeraren (felsäkert läge)", + "safeModeDescription": "Fregate är i felsäkert läge på grund av ett konfigurationsvalideringsfel." } diff --git a/web/public/locales/sv/views/events.json b/web/public/locales/sv/views/events.json index 9536f9b3d..89b9d1dd3 100644 --- a/web/public/locales/sv/views/events.json +++ b/web/public/locales/sv/views/events.json @@ -34,5 +34,7 @@ "markTheseItemsAsReviewed": "Markera dessa objekt som granskade", "detected": "upptäckt", "selected_one": "{{count}} valda", - "selected_other": "{{count}} valda" + "selected_other": "{{count}} valda", + "suspiciousActivity": "Misstänkt aktivitet", + "threateningActivity": "Hotande aktivitet" } diff --git a/web/public/locales/sv/views/explore.json b/web/public/locales/sv/views/explore.json index e8cecd73f..d4068b382 100644 --- a/web/public/locales/sv/views/explore.json +++ b/web/public/locales/sv/views/explore.json @@ -5,25 +5,141 @@ "embeddingsReindexing": { "startingUp": "Startar upp…", "estimatedTime": "Beräknad återstående tid:", - "finishingShortly": "Snart klar" + "finishingShortly": "Snart klar", + "context": "Utforska kan användas efter att spårade objektinbäddningar har slutförts omindexering.", + "step": { + "thumbnailsEmbedded": "Miniatyrbilder inbäddad: ", + "descriptionsEmbedded": "Beskrivningar inbäddade: ", + "trackedObjectsProcessed": "Spårade objekt bearbetad: " + } }, "title": "Utforska är inte tillgänglig", "downloadingModels": { "setup": { - "textModel": "Text modell" + "textModel": "Text modell", + "visionModel": "Visionsmodell", + "visionModelFeatureExtractor": "Funktionsutdragare för visionsmodell", + "textTokenizer": "Texttokeniserare" }, "tips": { - "documentation": "Läs dokumentationen" + "documentation": "Läs dokumentationen", + "context": "Du kanske vill omindexera inbäddningarna av dina spårade objekt när modellerna har laddats ner." }, - "error": "Ett fel har inträffat. Kontrollera Frigate loggarna." + "error": "Ett fel har inträffat. Kontrollera Frigate loggarna.", + "context": "Frigate laddar ner de nödvändiga inbäddningsmodellerna för att stödja den semantiska sökfunktionen. Detta kan ta flera minuter beroende på hastigheten på din nätverksanslutning." } }, "details": { - "timestamp": "tidsstämpel" + "timestamp": "tidsstämpel", + "item": { + "title": "Granska objektinformation", + "desc": "Granska objektinformation", + "button": { + "share": "Dela den här recensionen", + "viewInExplore": "Visa i Utforska" + }, + "tips": { + "mismatch_one": "{{count}} otillgängligt objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller detektering, eller så har de redan rensats/raderats.", + "mismatch_other": "{{count}} otillgängliga objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller upptäckt, eller så har de redan rensats/raderats.", + "hasMissingObjects": "Justera din konfiguration om du vill att Frigate ska spara spårade objekt för följande etiketter: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "En ny beskrivning har begärts från {{provider}}. Beroende på din leverantörs hastighet kan det ta lite tid att generera den nya beskrivningen.", + "updatedSublabel": "Underetiketten har uppdaterats.", + "updatedLPR": "Nummerplåt har uppdaterats.", + "audioTranscription": "Ljudtranskription har begärts." + }, + "error": { + "regenerate": "Kunde inte ringa {{provider}} för en ny beskrivning: {{errorMessage}}", + "updatedSublabelFailed": "Misslyckades med att uppdatera underetiketten: {{errorMessage}}", + "audioTranscription": "Misslyckades med att begära ljudtranskription: {{errorMessage}}", + "updatedLPRFailed": "Misslyckades med att uppdatera nummerplåten: {{errorMessage}}" + } + } + }, + "label": "Märka", + "editSubLabel": { + "title": "Redigera underetikett", + "desc": "Ange en ny underetikett för denna {{label}}", + "descNoLabel": "Ange en ny underetikett för det här spårade objektet" + }, + "editLPR": { + "title": "Redigera nummerplåt", + "desc": "Ange ett nytt nummerplåt för denna {{label}}", + "descNoLabel": "Ange ett nytt nummerplåt för detta spårade objekt" + }, + "snapshotScore": { + "label": "Ögonblicksbildspoäng" + }, + "topScore": { + "label": "Högsta poäng", + "info": "Topppoängen är den högsta medianpoängen för det spårade objektet, så denna kan skilja sig från poängen som visas på miniatyrbilden av sökresultatet." + }, + "score": { + "label": "Poäng" + }, + "recognizedLicensePlate": "Erkänd nummerplåt", + "estimatedSpeed": "Uppskattad hastighet", + "objects": "Objekt", + "camera": "Kamera", + "zones": "Zoner" }, "exploreMore": "Utforska fler {{label}} objekt", "type": { "details": "detaljer", - "video": "video" + "video": "video", + "snapshot": "ögonblicksbild", + "object_lifecycle": "objektets livscykel" + }, + "trackedObjectDetails": "Detaljer om spårade objekt", + "objectLifecycle": { + "title": "Objektets livscykel", + "noImageFound": "Ingen bild hittades för denna tidsstämpel.", + "createObjectMask": "Skapa objektmask", + "adjustAnnotationSettings": "Justera annoteringsinställningar", + "scrollViewTips": "Scrolla för att se de viktiga ögonblicken i detta objekts livscykel.", + "autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.", + "count": "{{first}} av {{second}}", + "lifecycleItemDesc": { + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Proportion", + "area": "Område" + }, + "visible": "{{label}} upptäckt", + "entered_zone": "{{label}} gick in i {{zones}}", + "active": "{{label}} blev aktiv", + "stationary": "{{label}} blev stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} upptäckt för {{label}}", + "other": "{{label}} igenkänd som {{attribute}}" + }, + "gone": "{{label}} vänster", + "heard": "{{label}} hört" + }, + "annotationSettings": { + "title": "Annoteringsinställningar", + "showAllZones": { + "title": "Visa alla zoner", + "desc": "Visa alltid zoner på ramar där objekt har kommit in i en zon." + }, + "offset": { + "label": "Annoteringsförskjutning", + "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Fältet annotation_offset kan dock användas för att justera detta.", + "documentation": "Läs dokumentationen ", + "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", + "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.", + "toast": { + "success": "Annoterings förskjutningen för {{camera}} har sparats i konfigurationsfilen. Starta om Frigate för att tillämpa dina ändringar." + } + } + }, + "trackedPoint": "Spårad punkt", + "carousel": { + "previous": "Föregående bild", + "next": "Nästa bild" + } } } diff --git a/web/public/locales/sv/views/faceLibrary.json b/web/public/locales/sv/views/faceLibrary.json index 763f7533c..78d0460b8 100644 --- a/web/public/locales/sv/views/faceLibrary.json +++ b/web/public/locales/sv/views/faceLibrary.json @@ -4,8 +4,10 @@ "confidence": "Säkerhet", "face": "Ansiktsdetaljer", "timestamp": "tidsstämpel", - "faceDesc": "Detaljer för ansiktet och tillhörande objekt", - "unknown": "Okänt" + "faceDesc": "Detaljer om det spårade objektet som genererade detta ansikte", + "unknown": "Okänt", + "subLabelScore": "Underetikett Poäng", + "scoreInfo": "Underetikettpoängen är den viktade poängen för alla igenkända ansiktskonfidenser, så detta kan skilja sig från poängen som visas på ögonblicksbilden." }, "description": { "placeholder": "Ange ett namn för denna samling", @@ -16,7 +18,10 @@ "steps": { "faceName": "Ange namn", "uploadFace": "Ladda upp bild på ansikte", - "nextSteps": "Nästa steg" + "nextSteps": "Nästa steg", + "description": { + "uploadFace": "Ladda upp en bild på {{name}} som visar deras ansikte framifrån. Bilden behöver inte beskäras till bara deras ansikte." + } }, "createFaceLibrary": { "title": "Skapa samling", @@ -25,12 +30,71 @@ "new": "Skapa nytt ansikte" }, "train": { - "title": "Träna" + "title": "Träna", + "aria": "Välj träna", + "empty": "Det finns inga ny försök till ansiktsigenkänning" }, "uploadFaceImage": { "title": "Ladda upp ansiktsbild", "desc": "Ladda upp en bild för att skanna efter ansikte och inkludera {{pageToggle}}" }, "selectItem": "Välj {{item}}", - "collections": "Samlingar" + "collections": "Samlingar", + "selectFace": "Välj ansikte", + "deleteFaceLibrary": { + "title": "Ta bort namn", + "desc": "Är du säker på att du vill ta bort samlingen {{name}}? Detta kommer att ta bort alla associerade ansikten permanent." + }, + "deleteFaceAttempts": { + "title": "Ta bort ansikten", + "desc_one": "Är du säker på att du vill ta bort {{count}} ansikte? Den här åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} ansikten? Den här åtgärden kan inte ångras." + }, + "imageEntry": { + "dropActive": "Släpp bilden här…", + "dropInstructions": "Dra och släpp en bild här, eller klicka för att välja", + "maxSize": "Maxstorlek: {{size}}MB", + "validation": { + "selectImage": "Välj en bildfil." + } + }, + "nofaces": "Inga ansikten tillgängliga", + "pixels": "{{area}}px", + "readTheDocs": "Läs dokumentationen", + "trainFaceAs": "Träna ansikte som:", + "trainFace": "Träna ansikte", + "toast": { + "success": { + "uploadedImage": "Bilden har laddats upp.", + "addFaceLibrary": "{{name}} har lagts till i ansiktsbiblioteket!", + "deletedFace_one": "{{count}} ansikte har raderats.", + "deletedFace_other": "{{count}} ansikten har raderats.", + "deletedName_one": "{{count}} ansikte har raderats.", + "deletedName_other": "{{count}} ansikten har raderats.", + "renamedFace": "Ansiktet har bytt namn till {{name}}", + "trainedFace": "Ansikte är tränant.", + "updatedFaceScore": "Ansikts poängen har uppdaterats." + }, + "error": { + "uploadingImageFailed": "Misslyckades med att ladda upp bilden: {{errorMessage}}", + "addFaceLibraryFailed": "Misslyckades med att ange ansiktsnamn: {{errorMessage}}", + "deleteFaceFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteNameFailed": "Misslyckades med att ta bort namnet: {{errorMessage}}", + "renameFaceFailed": "Misslyckades med att byta namn på ansikte: {{errorMessage}}", + "trainFailed": "Misslyckades med att träna: {{errorMessage}}", + "updateFaceScoreFailed": "Misslyckades med att uppdatera ansiktspoäng: {{errorMessage}}" + } + }, + "renameFace": { + "title": "Byt namn på ansikte", + "desc": "Ange ett nytt namn för {{name}}" + }, + "button": { + "deleteFaceAttempts": "Ta bort ansikten", + "addFace": "Lägg till ansikte", + "renameFace": "Byt namn på ansikte", + "deleteFace": "Ta bort ansikte", + "uploadImage": "Ladda upp bild", + "reprocessFace": "Återbearbeta ansiktet" + } } diff --git a/web/public/locales/sv/views/live.json b/web/public/locales/sv/views/live.json index ff6d2a4c2..25fb12598 100644 --- a/web/public/locales/sv/views/live.json +++ b/web/public/locales/sv/views/live.json @@ -2,8 +2,8 @@ "documentTitle": "Live - Frigate", "documentTitle.withCamera": "{{camera}} - Live - Frigate", "twoWayTalk": { - "enable": "Aktivera Two Way Talk", - "disable": "Avaktivera Two Way Talk" + "enable": "Aktivera tvåvägssamtal", + "disable": "Avaktivera tvåvägssamtal" }, "cameraAudio": { "disable": "Inaktivera kameraljud", @@ -42,7 +42,15 @@ "label": "Klicka i bilden för att centrera PTZ kamera" } }, - "presets": "PTZ kamera förinställningar" + "presets": "PTZ kamera förinställningar", + "focus": { + "in": { + "label": "Fokusera PTZ-kameran in" + }, + "out": { + "label": "Fokusera PTZ-kameran ut" + } + } }, "streamStats": { "enable": "Visa videostatistik", @@ -98,7 +106,8 @@ "objectDetection": "Objektsdetektering", "recording": "Inspelning", "snapshots": "Ögonblicksbilder", - "autotracking": "Autospårning" + "autotracking": "Autospårning", + "transcription": "Ljudtranskription" }, "effectiveRetainMode": { "modes": { @@ -154,5 +163,9 @@ }, "history": { "label": "Visa historiskt videomaterial" + }, + "transcription": { + "enable": "Aktivera live-ljudtranskription", + "disable": "Inaktivera live-ljudtranskription" } } diff --git a/web/public/locales/sv/views/settings.json b/web/public/locales/sv/views/settings.json index df1de30e0..86d9bc787 100644 --- a/web/public/locales/sv/views/settings.json +++ b/web/public/locales/sv/views/settings.json @@ -8,7 +8,9 @@ "masksAndZones": "Maskerings- och zonverktyg - Frigate", "enrichments": "Förbättringsinställningar - Frigate", "frigatePlus": "Frigate+ Inställningar - Frigate", - "notifications": "Notifikations Inställningar - Frigate" + "notifications": "Notifikations Inställningar - Frigate", + "motionTuner": "Rörelsetuner - Frigate", + "object": "Felsöka - Frigate" }, "general": { "title": "Allmänna Inställningar", @@ -44,7 +46,8 @@ "firstWeekday": { "sunday": "Söndag", "monday": "Måndag", - "label": "Första Veckodag" + "label": "Första Veckodag", + "desc": "Den dag då veckorna i översynskalendern börjar." }, "title": "Kalender" }, @@ -66,18 +69,74 @@ "enrichments": { "unsavedChanges": "Osparade Förbättringsinställningar", "birdClassification": { - "title": "Fågel klassificering" + "title": "Fågel klassificering", + "desc": "Fågelklassificering identifierar kända fåglar med hjälp av en kvantiserad Tensorflow-modell. När en känd fågel känns igen läggs dess vanliga namn till som en underetikett. Denna information inkluderas i användargränssnittet, filter och i aviseringar." }, - "title": "Förbättringsinställningar" + "title": "Förbättringsinställningar", + "semanticSearch": { + "title": "Semantisk sökning", + "desc": "Semantisk sökning i Frigate låter dig hitta spårade objekt i dina granskningsobjekt med hjälp av antingen själva bilden, en användardefinierad textbeskrivning eller en automatiskt genererad.", + "readTheDocumentation": "Läs dokumentationen", + "reindexNow": { + "label": "Omindexera nu", + "desc": "Omindexering kommer att generera inbäddningar för alla spårade objekt. Den här processen körs i bakgrunden och kan maximera din CPU och ta en hel del tid beroende på antalet spårade objekt du har.", + "confirmTitle": "Bekräfta omindexering", + "confirmDesc": "Är du säker på att du vill omindexera alla spårade objektinbäddningar? Den här processen körs i bakgrunden men den kan maximera din processor och ta en hel del tid. Du kan se förloppet på Utforska-sidan.", + "confirmButton": "Omindexera", + "success": "Omindexeringen har startat.", + "alreadyInProgress": "Omindexering pågår redan.", + "error": "Misslyckades med att starta omindexering: {{errorMessage}}" + }, + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för semantiska sökinbäddningar.", + "small": { + "title": "små", + "desc": "Att använda small använder en kvantiserad version av modellen som använder mindre RAM och körs snabbare på CPU med en mycket försumbar skillnad i inbäddningskvalitet." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder hela Jina-modellen och körs automatiskt på GPU:n om tillämpligt." + } + } + }, + "faceRecognition": { + "desc": "Ansiktsigenkänning gör att personer kan tilldelas namn och när deras ansikte känns igen kommer Frigate att tilldela personens namn som en underetikett. Denna information finns i användargränssnittet, filter och i aviseringar.", + "readTheDocumentation": "Läs dokumentationen", + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för ansiktsigenkänning.", + "small": { + "title": "små", + "desc": "Att använda small använder en FaceNet-modell för ansiktsinbäddning som körs effektivt på de flesta processorer." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder en ArcFace-modell för ansiktsinbäddning och körs automatiskt på GPU:n om tillämpligt." + } + }, + "title": "Ansikts igenkänning" + }, + "licensePlateRecognition": { + "title": "Nummerplåt Erkännande", + "desc": "Frigate kan känna igen nummerplåt på fordon och automatiskt lägga till de upptäckta tecknen i fältet recognized_license_plate eller ett känt namn som en underetikett till objekt av typen bil. Ett vanligt användningsfall kan vara att läsa nummerplåtor på bilar som kör in på en uppfart eller bilar som passerar på en gata.", + "readTheDocumentation": "Läs dokumentationen" + }, + "restart_required": "Omstart krävs (berikningsinställningar har ändrats)", + "toast": { + "success": "Inställningarna för berikning har sparats. Starta om Frigate för att tillämpa dina ändringar." + } }, "menu": { - "ui": "UI", + "ui": "Användargränssnitt", "cameras": "Kamera Inställningar", "masksAndZones": "Masker / Områden", "users": "Användare", "notifications": "Notifikationer", "frigateplus": "Frigate+", - "enrichments": "Förbättringar" + "enrichments": "Förbättringar", + "motionTuner": "Rörelsemottagare", + "debug": "Felsök" }, "dialog": { "unsavedChanges": { diff --git a/web/public/locales/sv/views/system.json b/web/public/locales/sv/views/system.json index d10bf2e1d..15d814f54 100644 --- a/web/public/locales/sv/views/system.json +++ b/web/public/locales/sv/views/system.json @@ -4,9 +4,11 @@ "general": "Allmän statistik - Frigate", "cameras": "Kamerastatistik - Frigate", "logs": { - "frigate": "Frigate loggar - Frigate", - "go2rtc": "Go2RTC Loggar - Frigate" - } + "frigate": "Frigate-loggar - Frigate", + "go2rtc": "Go2RTC-loggar - Frigate", + "nginx": "Nginx-loggar - Frigate" + }, + "enrichments": "Förbättringsstatistik - Frigate" }, "logs": { "copy": { @@ -32,19 +34,53 @@ } }, "title": "System", - "metrics": "System detaljer", + "metrics": "Systemdetaljer", "general": { "title": "Generellt", "detector": { - "title": "Detektorer" + "title": "Detektorer", + "inferenceSpeed": "Detektorns inferenshastighet", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektorns CPU-användning", + "memoryUsage": "Detektor minnes användning" }, "hardwareInfo": { "title": "Hårdvaruinformation", "gpuUsage": "GPU-användning", - "gpuMemory": "GPU-minne" + "gpuMemory": "GPU-minne", + "gpuEncoder": "GPU-kodare", + "gpuDecoder": "GPU-avkodare", + "gpuInfo": { + "nvidiaSMIOutput": { + "vbios": "VBios-information: {{vbios}}", + "title": "Nvidia SMI utdata", + "name": "Namn: {{name}}", + "driver": "Drivrutin: {{driver}}", + "cudaComputerCapability": "CUDA beräknings kapacitet: {{cuda_compute}}" + }, + "closeInfo": { + "label": "Stäng GPU-info" + }, + "copyInfo": { + "label": "Kopiera GPU-info" + }, + "toast": { + "success": "Kopierade GPU-info till urklipp" + }, + "vainfoOutput": { + "title": "Vainfo resultat", + "returnCode": "Returkod: {{code}}", + "processOutput": "Bearbeta utdata:", + "processError": "Processfel:" + } + }, + "npuUsage": "NPU-användning", + "npuMemory": "NPU-minne" }, "otherProcesses": { - "title": "Övriga processer" + "title": "Övriga processer", + "processCpuUsage": "Process CPU-användning", + "processMemoryUsage": "Processminnesanvändning" } }, "storage": { @@ -55,17 +91,46 @@ "unused": { "title": "Oanvänt", "tips": "Det här värdet kanske inte korrekt representerar det lediga utrymmet tillgängligt för Frigate om du har andra filer lagrade på din hårddisk utöver Frigates inspelningar. Frigate spårar inte lagringsanvändning utanför sina egna inspelningar." - } + }, + "title": "Kamera lagring", + "camera": "Kamera", + "unusedStorageInformation": "Information om oanvänd lagring" + }, + "title": "Lagring", + "overview": "Översikt", + "recordings": { + "title": "Inspelningar", + "tips": "Detta värde representerar den totala lagringsmängden som används av inspelningarna i Frigates databas. Frigate spårar inte lagringsanvändningen för alla filer på din disk.", + "earliestRecording": "Tidigast tillgängliga inspelning:" } }, "cameras": { "title": "Kameror", "overview": "Översikt", "info": { - "aspectRatio": "bildförhållande" + "aspectRatio": "bildförhållande", + "cameraProbeInfo": "{{camera}} Kamerasondinformation", + "streamDataFromFFPROBE": "Strömdata erhålls med ffprobe.", + "codec": "Codec:", + "resolution": "Upplösning:", + "fps": "FPS:", + "unknown": "Okänd", + "audio": "Audio:", + "error": "Fel: {{error}}", + "tips": { + "title": "Kamera sond information" + }, + "fetching": "Hämtar kamera data", + "stream": "Ström {{idx}}", + "video": "Video:" }, "label": { - "detect": "detektera" - } + "detect": "detektera", + "camera": "kamera", + "skipped": "hoppade över", + "ffmpeg": "FFmpeg", + "capture": "spela in" + }, + "framesAndDetections": "Ramar / Detektioner" } } diff --git a/web/public/locales/th/common.json b/web/public/locales/th/common.json index c7044976d..b92078797 100644 --- a/web/public/locales/th/common.json +++ b/web/public/locales/th/common.json @@ -246,5 +246,6 @@ "feet": "ฟุต", "meters": "เมตร" } - } + }, + "readTheDocumentation": "อ่านเอกสาร" } diff --git a/web/public/locales/tr/common.json b/web/public/locales/tr/common.json index e23b402ca..ebb4f9b1e 100644 --- a/web/public/locales/tr/common.json +++ b/web/public/locales/tr/common.json @@ -166,7 +166,15 @@ "ru": "Русский (Rusça)", "yue": "粵語 (Kantonca)", "th": "ไทย (Tayca)", - "ca": "Català (Katalanca)" + "ca": "Català (Katalanca)", + "ptBR": "Português brasileiro (Brezilya Portekizcesi)", + "sr": "Српски (Sırpça)", + "sl": "Slovenščina (Slovence)", + "lt": "Lietuvių (Litvanyaca)", + "bg": "Български (Bulgarca)", + "gl": "Galego (Galiçyaca)", + "id": "Bahasa Indonesia (Endonezce)", + "ur": "اردو (Urduca)" }, "withSystem": "Sistem", "theme": { @@ -264,5 +272,6 @@ "viewer": "Görüntüleyici", "admin": "Yönetici", "desc": "Yöneticiler Frigate arayüzündeki bütün özelliklere tam erişim sahibidir. Görüntüleyiciler ise yalnızca kameraları, eski görüntüleri ve inceleme öğelerini görüntülemekle sınırlıdır." - } + }, + "readTheDocumentation": "Dökümantasyonu oku" } diff --git a/web/public/locales/uk/common.json b/web/public/locales/uk/common.json index 029364971..70f97181c 100644 --- a/web/public/locales/uk/common.json +++ b/web/public/locales/uk/common.json @@ -152,7 +152,15 @@ "en": "Англійська", "yue": "粵語 (Кантонська)", "th": "ไทย (Тайська)", - "ca": "Català (Каталанська)" + "ca": "Català (Каталанська)", + "ptBR": "Português brasileiro (Бразильська португальська)", + "sr": "Српски (Сербська)", + "sl": "Slovenščina (Словенська)", + "lt": "Lietuvių (Литовська)", + "bg": "Български (Болгарська)", + "gl": "Galego (Галісійська)", + "id": "Bahasa Indonesia (Індонезійська)", + "ur": "اردو (Урду)" }, "system": "Система", "systemMetrics": "Системна метріка", @@ -262,5 +270,6 @@ "desc": "Сторінка не знайдена", "title": "404" }, - "selectItem": "Вибрати {{item}}" + "selectItem": "Вибрати {{item}}", + "readTheDocumentation": "Прочитати документацію" } diff --git a/web/public/locales/uk/components/dialog.json b/web/public/locales/uk/components/dialog.json index 43cb9bd9b..fadbb19e0 100644 --- a/web/public/locales/uk/components/dialog.json +++ b/web/public/locales/uk/components/dialog.json @@ -110,5 +110,12 @@ "content": "Цю сторінку буде перезавантажено за {{countdown}} секунд.", "button": "Примусово перезавантажити" } + }, + "imagePicker": { + "selectImage": "Вибір мініатюри відстежуваного об'єкта", + "search": { + "placeholder": "Пошук за міткою або підміткою..." + }, + "noImages": "Для цієї камери не знайдено мініатюр" } } diff --git a/web/public/locales/uk/components/filter.json b/web/public/locales/uk/components/filter.json index 95c01f349..73968cfbe 100644 --- a/web/public/locales/uk/components/filter.json +++ b/web/public/locales/uk/components/filter.json @@ -122,5 +122,13 @@ "placeholder": "Введіть для пошуку номерні знаки…", "noLicensePlatesFound": "Номерних знаків не знайдено.", "selectPlatesFromList": "Виберіть одну або кілька пластин зі списку." + }, + "classes": { + "label": "Заняття", + "all": { + "title": "Усі класи" + }, + "count_one": "Клас {{count}}", + "count_other": "{{count}} Класи" } } diff --git a/web/public/locales/uk/views/configEditor.json b/web/public/locales/uk/views/configEditor.json index c9a664113..0e3ef13cb 100644 --- a/web/public/locales/uk/views/configEditor.json +++ b/web/public/locales/uk/views/configEditor.json @@ -12,5 +12,7 @@ "copyConfig": "Скопіювати конфігурацію", "saveOnly": "Тільки зберегти", "configEditor": "Налаштування редактора", - "confirm": "Вийти без збереження?" + "confirm": "Вийти без збереження?", + "safeConfigEditor": "Редактор конфігурації (безпечний режим)", + "safeModeDescription": "Фрегат перебуває в безпечному режимі через помилку перевірки конфігурації." } diff --git a/web/public/locales/uk/views/events.json b/web/public/locales/uk/views/events.json index e84c418ec..f94c99f0c 100644 --- a/web/public/locales/uk/views/events.json +++ b/web/public/locales/uk/views/events.json @@ -34,5 +34,7 @@ "label": "Переглянути нові елементи огляду", "button": "Нові матеріали для перегляду" }, - "detected": "виявлено" + "detected": "виявлено", + "suspiciousActivity": "Підозріла активність", + "threateningActivity": "Загрожувальна діяльність" } diff --git a/web/public/locales/uk/views/explore.json b/web/public/locales/uk/views/explore.json index cdbcdb6ee..494801e4b 100644 --- a/web/public/locales/uk/views/explore.json +++ b/web/public/locales/uk/views/explore.json @@ -101,12 +101,14 @@ "success": { "updatedLPR": "Номерний знак успішно оновлено.", "updatedSublabel": "Підмітку успішно оновлено.", - "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час." + "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час.", + "audioTranscription": "Запит на аудіотранскрипцію успішно надіслано." }, "error": { "regenerate": "Не вдалося звернутися до {{provider}} для отримання нового опису: {{errorMessage}}", "updatedSublabelFailed": "Не вдалося оновити підмітку: {{errorMessage}}", - "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}" + "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}", + "audioTranscription": "Не вдалося надіслати запит на транскрипцію аудіо: {{errorMessage}}" } }, "button": { @@ -158,7 +160,10 @@ } }, "expandRegenerationMenu": "Розгорнути меню регенерації", - "regenerateFromSnapshot": "Відновити зі знімка" + "regenerateFromSnapshot": "Відновити зі знімка", + "score": { + "label": "Рахунок" + } }, "dialog": { "confirmDelete": { @@ -193,6 +198,14 @@ }, "deleteTrackedObject": { "label": "Видалити цей відстежуваний об'єкт" + }, + "addTrigger": { + "label": "Додати тригер", + "aria": "Додати тригер для цього відстежуваного об'єкта" + }, + "audioTranscription": { + "label": "Транскрибувати", + "aria": "Запит на аудіотранскрипцію" } }, "noTrackedObjects": "Відстежуваних об'єктів не знайдено", @@ -205,5 +218,11 @@ "video": "відео", "object_lifecycle": "життєвий цикл об'єкта" }, - "exploreMore": "Дослідіть більше об'єктів {{label}}" + "exploreMore": "Дослідіть більше об'єктів {{label}}", + "aiAnalysis": { + "title": "Аналіз ШІ" + }, + "concerns": { + "label": "Проблеми" + } } diff --git a/web/public/locales/uk/views/live.json b/web/public/locales/uk/views/live.json index 27a8c518a..e8ce0aa8d 100644 --- a/web/public/locales/uk/views/live.json +++ b/web/public/locales/uk/views/live.json @@ -85,6 +85,14 @@ "center": { "label": "Клацніть у кадрі, щоб відцентрувати камеру PTZ" } + }, + "focus": { + "in": { + "label": "Фокус PTZ-камери" + }, + "out": { + "label": "Вихід PTZ-камери для фокусування" + } } }, "editLayout": { @@ -94,7 +102,7 @@ "label": "Редагувати групу камер" } }, - "documentTitle": "Прямий трансляція - Frigate", + "documentTitle": "Пряма трансляція - Frigate", "documentTitle.withCamera": "{{camera}} - Пряма трансляція - Frigate", "lowBandwidthMode": "Економічний режим", "twoWayTalk": { @@ -142,7 +150,8 @@ "recording": "Записування", "snapshots": "Знімки", "audioDetection": "Виявлення звуку", - "autotracking": "Автотрекiнг" + "autotracking": "Автотрекiнг", + "transcription": "Аудіотранскрипція" }, "history": { "label": "Показати історичні кадри" @@ -154,5 +163,9 @@ "active_objects": "Активні об'єкти" }, "notAllTips": "Ваш {{source}} конфігурацію збереження записів встановлено на режим: {{effectiveRetainMode}}, тому цей запис на вимогу збереже лише сегменти з {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Увімкнути транскрипцію аудіо в реальному часі", + "disable": "Вимкнути транскрипцію аудіо в реальному часі" } } diff --git a/web/public/locales/uk/views/settings.json b/web/public/locales/uk/views/settings.json index a5e7d511f..77ccc09a1 100644 --- a/web/public/locales/uk/views/settings.json +++ b/web/public/locales/uk/views/settings.json @@ -86,7 +86,44 @@ "title": "Огляд", "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. " }, - "title": "Налаштування камери" + "title": "Налаштування камери", + "object_descriptions": { + "title": "Генеративні описи об'єктів штучного інтелекту", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери. Якщо вимкнено, для елементів огляду на цій камері не запитуватимуться згенеровані штучним інтелектом описи." + }, + "addCamera": "Додати нову камеру", + "editCamera": "Редагувати камеру:", + "selectCamera": "Виберіть камеру", + "backToSettings": "Назад до налаштувань камери", + "cameraConfig": { + "add": "Додати камеру", + "edit": "Редагувати камеру", + "description": "Налаштуйте параметри камери, включаючи потокові входи та ролі.", + "name": "Назва камери", + "nameRequired": "Потрібно вказати назву камери", + "nameInvalid": "Назва камери повинна містити лише літери, цифри, символи підкреслення або дефіси", + "namePlaceholder": "наприклад, вхідні_двері", + "enabled": "Увімкнено", + "ffmpeg": { + "inputs": "Вхідні потоки", + "path": "Шлях потоку", + "pathRequired": "Шлях потоку обов'язковий", + "pathPlaceholder": "'rtsp://...", + "roles": "Ролі", + "rolesRequired": "Потрібна хоча б одна роль", + "rolesUnique": "Кожна роль (аудіо, виявлення, запис) може бути призначена лише одному потоку", + "addInput": "Додати вхідний потік", + "removeInput": "Вилучити вхідний потік", + "inputsRequired": "Потрібен принаймні один вхідний потік" + }, + "toast": { + "success": "Камеру {{cameraName}} успішно збережено" + } + } }, "masksAndZones": { "motionMasks": { @@ -308,7 +345,12 @@ "tips": "

Поля руху


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

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

Шляхи


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

" + } }, "classification": { "licensePlateRecognition": { @@ -498,7 +540,7 @@ "classification": "Налаштування класифікації – Фрегат", "masksAndZones": "Редактор масок та зон – Фрегат", "motionTuner": "Тюнер руху - Фрегат", - "general": "Основна налаштування – Frigate", + "general": "Основна Налаштування – Frigate", "frigatePlus": "Налаштування Frigate+ – Frigate", "enrichments": "Налаштуваннях збагачення – Frigate" }, @@ -681,6 +723,101 @@ "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі тензорного потоку. Коли відомого птаха розпізнано, його загальну назву буде додано як підмітку. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях.", "title": "Класифікація птахів" }, - "title": "Налаштуваннях збагачення" + "title": "Налаштуваннях Збагачення" + }, + "triggers": { + "documentTitle": "Тригери", + "management": { + "title": "Управління тригерами", + "desc": "Керуйте тригерами для {{camera}}. Використовуйте тип мініатюри для спрацьовування на схожих мініатюрах до вибраного об’єкта відстеження, а тип опису – для спрацьовування на схожих описах до вказаного вами тексту." + }, + "addTrigger": "Додати Тригер", + "table": { + "name": "Ім'я", + "type": "Тип", + "content": "Зміст", + "threshold": "Поріг", + "actions": "Дії", + "noTriggers": "Для цієї камери не налаштовано жодних тригерів.", + "edit": "Редагувати", + "deleteTrigger": "Видалити тригер", + "lastTriggered": "Остання активація" + }, + "type": { + "thumbnail": "Мініатюра", + "description": "Опис" + }, + "actions": { + "alert": "Позначити як сповіщення", + "notification": "Надіслати сповіщення" + }, + "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": "Виберіть тип тригера" + }, + "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}}" + } + } } } diff --git a/web/public/locales/uk/views/system.json b/web/public/locales/uk/views/system.json index b1472f7a7..44d9a2d60 100644 --- a/web/public/locales/uk/views/system.json +++ b/web/public/locales/uk/views/system.json @@ -129,7 +129,12 @@ "tips": "Це значення відображає загальний обсяг пам’яті, що використовується записами в базі даних Frigate. Frigate не відстежує використання пам’яті для всіх файлів на вашому диску.", "earliestRecording": "Найдавніший доступний запис:" }, - "title": "Зберігання" + "title": "Зберігання", + "shm": { + "title": "Розподіл спільної пам'яті (SHM)", + "warning": "Поточний розмір SHM, що становить {{total}} МБ, замалий. Збільште його принаймні до {{min_shm}} МБ.", + "readTheDocumentation": "Прочитайте документацію" + } }, "lastRefreshed": "Останнє оновлення: ", "stats": { diff --git a/web/public/locales/ur/common.json b/web/public/locales/ur/common.json index dbf35b3b6..37ff068c5 100644 --- a/web/public/locales/ur/common.json +++ b/web/public/locales/ur/common.json @@ -34,5 +34,6 @@ "month_other": "{{time}} مہینے", "hour_one": "{{time}} گھنٹہ", "hour_other": "{{time}} گھنٹے" - } + }, + "readTheDocumentation": "دستاویز پڑھیں" } diff --git a/web/public/locales/vi/common.json b/web/public/locales/vi/common.json index af34c5ee3..843675f42 100644 --- a/web/public/locales/vi/common.json +++ b/web/public/locales/vi/common.json @@ -121,7 +121,15 @@ }, "yue": "粵語 (Tiếng Quảng Đông)", "ca": "Català (Tiếng Catalan)", - "th": "ไทย (Tiếng Thái)" + "th": "ไทย (Tiếng Thái)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "sr": "Српски (Serbian)", + "sl": "Slovenščina (Slovenian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)" }, "system": "Hệ thống", "systemMetrics": "Thông số hệ thống", @@ -257,5 +265,6 @@ "title": "Không tìm thấy", "desc": "Trang bạn đang tìm không tồn tại" }, - "selectItem": "Chọn mục {{item}}" + "selectItem": "Chọn mục {{item}}", + "readTheDocumentation": "Đọc tài liệu" } diff --git a/web/public/locales/vi/components/dialog.json b/web/public/locales/vi/components/dialog.json index 53b1226b1..5cef1da16 100644 --- a/web/public/locales/vi/components/dialog.json +++ b/web/public/locales/vi/components/dialog.json @@ -108,5 +108,12 @@ "placeholder": "Nhập tên cho tìm kiếm của bạn", "overwrite": "{{searchName}} đã tồn tại. Lưu sẽ ghi đè lên giá trị hiện có." } + }, + "imagePicker": { + "selectImage": "Chọn hình thu nhỏ của đối tượng cần theo dõi", + "search": { + "placeholder": "Tìm theo nhãn hoặc nhãn phụ..." + }, + "noImages": "Không tìm thấy hình thu nhỏ cho camera này" } } diff --git a/web/public/locales/vi/components/filter.json b/web/public/locales/vi/components/filter.json index 1570067ab..a640f7267 100644 --- a/web/public/locales/vi/components/filter.json +++ b/web/public/locales/vi/components/filter.json @@ -122,5 +122,13 @@ "title": "Tất cả Khu vực", "short": "Khu vực" } + }, + "classes": { + "label": "Các nhãn nhận diện", + "all": { + "title": "Tất cả nhãn nhận diện" + }, + "count_one": "{{count}} Nhãn nhận diện", + "count_other": "{{count}} Các nhãn nhận diện" } } diff --git a/web/public/locales/vi/views/configEditor.json b/web/public/locales/vi/views/configEditor.json index a9a0c4f82..a2ffce4a9 100644 --- a/web/public/locales/vi/views/configEditor.json +++ b/web/public/locales/vi/views/configEditor.json @@ -12,5 +12,7 @@ } }, "configEditor": "Trình chỉnh sửa cấu hình", - "documentTitle": "Trình chỉnh sửa - Frigate" + "documentTitle": "Trình chỉnh sửa - Frigate", + "safeConfigEditor": "Chỉnh sửa cấu hình (Chế độ an toàn)", + "safeModeDescription": "Frigate đang ở chế độ an toàn do lỗi kiểm tra cấu hình." } diff --git a/web/public/locales/vi/views/events.json b/web/public/locales/vi/views/events.json index 4259ab2cc..c3bdad497 100644 --- a/web/public/locales/vi/views/events.json +++ b/web/public/locales/vi/views/events.json @@ -34,5 +34,7 @@ "button": "Các mục mới cần xem xét" }, "markAsReviewed": "Đánh dấu là đã xem xét", - "markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét" + "markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét", + "suspiciousActivity": "Hoạt động đáng ngờ", + "threateningActivity": "Hoạt động đe dọa" } diff --git a/web/public/locales/vi/views/explore.json b/web/public/locales/vi/views/explore.json index 99e4a65d5..82b3b7ad0 100644 --- a/web/public/locales/vi/views/explore.json +++ b/web/public/locales/vi/views/explore.json @@ -60,12 +60,14 @@ "error": { "updatedSublabelFailed": "Không thể cập nhật nhãn phụ: {{errorMessage}}", "updatedLPRFailed": "Không thể cập nhật biển số xe: {{errorMessage}}", - "regenerate": "Không thể gọi {{provider}} để lấy mô tả mới: {{errorMessage}}" + "regenerate": "Không thể gọi {{provider}} để lấy mô tả mới: {{errorMessage}}", + "audioTranscription": "Không thể yêu cầu phiên âm: {{errorMessage}}" }, "success": { "regenerate": "Một mô tả mới đã được yêu cầu từ {{provider}}. Tùy thuộc vào tốc độ của nhà cung cấp của bạn, mô tả mới có thể mất một chút thời gian để tạo lại.", "updatedLPR": "Cập nhật biển số xe thành công.", - "updatedSublabel": "Cập nhật nhãn phụ thành công." + "updatedSublabel": "Cập nhật nhãn phụ thành công.", + "audioTranscription": "Đã yêu cầu phiên âm thành công." } }, "tips": { @@ -115,6 +117,9 @@ "title": "Chỉnh sửa biển số xe", "desc": "Nhập một giá trị biển số xe mới cho {{label}} này", "descNoLabel": "Nhập một giá trị biển số xe mới cho đối tượng được theo dõi này" + }, + "score": { + "label": "Điểm tin cậy" } }, "itemMenu": { @@ -144,6 +149,14 @@ }, "deleteTrackedObject": { "label": "Xóa đối tượng được theo dõi này" + }, + "addTrigger": { + "label": "Thêm sự kiện kích hoạt", + "aria": "Thêm sự kiện kích hoạt cho đối tượng này." + }, + "audioTranscription": { + "label": "Phiên âm", + "aria": "Yêu cầu phiên âm" } }, "exploreIsUnavailable": { @@ -201,5 +214,11 @@ "fetchingTrackedObjectsFailed": "Lỗi khi tìm nạp các đối tượng được theo dõi: {{errorMessage}}", "documentTitle": "Khám phá - Frigate", "generativeAI": "AI Tạo sinh", - "trackedObjectsCount_other": "{{count}} đối tượng được theo dõi " + "trackedObjectsCount_other": "{{count}} đối tượng được theo dõi ", + "aiAnalysis": { + "title": "Phân tích bằng AI" + }, + "concerns": { + "label": "Mối lo ngại" + } } diff --git a/web/public/locales/vi/views/live.json b/web/public/locales/vi/views/live.json index 3e8ab44f6..ec194ba32 100644 --- a/web/public/locales/vi/views/live.json +++ b/web/public/locales/vi/views/live.json @@ -71,7 +71,15 @@ "label": "Di chuyển camera PTZ sang phải" } }, - "presets": "Các thiết lập sẵn cho camera PTZ" + "presets": "Các thiết lập sẵn cho camera PTZ", + "focus": { + "in": { + "label": "Lấy nét gần (camera PTZ)" + }, + "out": { + "label": "Lấy nét xa (camera PTZ)" + } + } }, "manualRecording": { "playInBackground": { @@ -142,7 +150,8 @@ "recording": "Ghi hình", "snapshots": "Ảnh chụp", "audioDetection": "Phát hiện âm thanh", - "autotracking": "Tự động theo dõi" + "autotracking": "Tự động theo dõi", + "transcription": "Phiên âm" }, "history": { "label": "Hiện cảnh quay lịch sử" @@ -154,5 +163,9 @@ "active_objects": "Đối tượng hoạt động" }, "notAllTips": "Cấu hình giữ lại ghi hình {{source}} của bạn được đặt là mode: {{effectiveRetainMode}}, vì vậy lần ghi hình theo yêu cầu này chỉ giữ lại các đoạn có {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Bật phiên âm trực tiếp", + "disable": "Tắt phiên âm trực tiếp" } } diff --git a/web/public/locales/vi/views/settings.json b/web/public/locales/vi/views/settings.json index 4f0972425..15efa095e 100644 --- a/web/public/locales/vi/views/settings.json +++ b/web/public/locales/vi/views/settings.json @@ -142,6 +142,43 @@ "streams": { "title": "Luồng phát", "desc": "Tạm thời vô hiệu hóa một camera cho đến khi Frigate khởi động lại. Vô hiệu hóa một camera sẽ dừng hoàn toàn quá trình xử lý các luồng của camera này của Frigate. Việc phát hiện, ghi hình và gỡ lỗi sẽ không khả dụng.
Lưu ý: Điều này không vô hiệu hóa các luồng phát lại của go2rtc." + }, + "object_descriptions": { + "title": "Mô tả đối tượng bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả đối tượng bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các đối tượng được theo dõi trên camera này." + }, + "review_descriptions": { + "title": "Mô tả đánh giá bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả xem lại bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các mục xem lại trên camera này." + }, + "addCamera": "Thêm Camera mới", + "editCamera": "Chỉnh sửa Camera:", + "selectCamera": "Chọn Camera", + "backToSettings": "Quay lại cài đặt Camera", + "cameraConfig": { + "add": "Thêm Camera", + "edit": "Chỉnh sửa Camera", + "description": "Cấu hình Camera, bao gồm luồng đầu vào và vai trò.", + "name": "Tên Camera", + "nameRequired": "Yêu cầu nhập tên Camera", + "nameInvalid": "Tên Camera chỉ được chứa chữ cái, số, dấu gạch dưới hoặc dấu gạch ngang", + "namePlaceholder": "Ví dụ: front_door", + "enabled": "Bật", + "ffmpeg": { + "inputs": "Luồng đầu vào", + "path": "Đường dẫn luồng", + "pathRequired": "Yêu cầu nhập đường dẫn luồng", + "pathPlaceholder": "rtsp://...", + "roles": "Vai trò", + "rolesRequired": "Cần ít nhất một vai trò", + "rolesUnique": "Mỗi vai trò (âm thanh, phát hiện, ghi hình) chỉ có thể được gán cho một luồng duy nhất", + "addInput": "Thêm luồng đầu vào", + "removeInput": "Xóa luồng đầu vào", + "inputsRequired": "Cần ít nhất một luồng đầu vào" + }, + "toast": { + "success": "Camera {{cameraName}} đã được lưu thành công" + } } }, "masksAndZones": { @@ -381,6 +418,11 @@ "desc": "Hiển thị các hộp xung quanh các khu vực phát hiện có chuyển động", "tips": "

Hộp chuyển động


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

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

Đường đi


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

" } }, "users": { @@ -612,5 +654,11 @@ "cameraSetting": { "camera": "Camera", "noCamera": "Không có Camera" + }, + "triggers": { + "documentTitle": "Sự kiện kích hoạt", + "management": { + "title": "Quản lý sự kiện kích hoạt" + } } } diff --git a/web/public/locales/yue-Hant/common.json b/web/public/locales/yue-Hant/common.json index 03f4f89b4..60bb2a2c6 100644 --- a/web/public/locales/yue-Hant/common.json +++ b/web/public/locales/yue-Hant/common.json @@ -248,5 +248,6 @@ "documentTitle": "找不到頁面 - Frigate", "desc": "找不到頁面", "title": "404" - } + }, + "readTheDocumentation": "閱讀文件" } diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json index 1c253aee4..2c028bcb2 100644 --- a/web/public/locales/zh-CN/common.json +++ b/web/public/locales/zh-CN/common.json @@ -181,7 +181,15 @@ "cs": "捷克语 (Čeština)", "yue": "粤语 (粵語)", "th": "泰语(ไทย)", - "ca": "加泰罗尼亚语 (Català )" + "ca": "加泰罗尼亚语 (Català )", + "ptBR": "巴西葡萄牙语 (Português brasileiro)", + "sr": "塞尔维亚语 (Српски)", + "sl": "斯洛文尼亚语 (Slovenščina)", + "lt": "立陶宛语 (Lietuvių)", + "bg": "保加利亚语 (Български)", + "gl": "加利西亚语 (Galego)", + "id": "印度尼西亚语 (Bahasa Indonesia)", + "ur": "乌尔都语 (اردو)" }, "appearance": "外观", "darkMode": { @@ -257,5 +265,6 @@ "title": "404", "desc": "页面未找到" }, - "selectItem": "选择 {{item}}" + "selectItem": "选择 {{item}}", + "readTheDocumentation": "阅读文档" } diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json index e7670d1e6..980c2c6a4 100644 --- a/web/public/locales/zh-CN/components/dialog.json +++ b/web/public/locales/zh-CN/components/dialog.json @@ -116,5 +116,12 @@ "markAsReviewed": "标记为已核查", "deleteNow": "立即删除" } + }, + "imagePicker": { + "selectImage": "选择追踪对象的缩略图", + "search": { + "placeholder": "通过标签或子标签搜索……" + }, + "noImages": "未在此摄像头找到缩略图" } } diff --git a/web/public/locales/zh-CN/components/filter.json b/web/public/locales/zh-CN/components/filter.json index 5824a421d..e631a1065 100644 --- a/web/public/locales/zh-CN/components/filter.json +++ b/web/public/locales/zh-CN/components/filter.json @@ -123,5 +123,13 @@ "placeholder": "输入以搜索车牌…", "noLicensePlatesFound": "未找到车牌。", "selectPlatesFromList": "从列表中选择一个或多个车牌。" + }, + "classes": { + "label": "分类", + "all": { + "title": "所有分类" + }, + "count_one": "{{count}} 个分类", + "count_other": "{{count}} 个分类" } } diff --git a/web/public/locales/zh-CN/components/player.json b/web/public/locales/zh-CN/components/player.json index df6648048..0336c32a1 100644 --- a/web/public/locales/zh-CN/components/player.json +++ b/web/public/locales/zh-CN/components/player.json @@ -11,7 +11,7 @@ "title": "视频流离线", "desc": "未在 {{cameraName}} 的 detect 流上接收到任何帧,请检查错误日志" }, - "cameraDisabled": "摄像机已禁用", + "cameraDisabled": "摄像头已禁用", "stats": { "streamType": { "title": "流类型:", diff --git a/web/public/locales/zh-CN/views/configEditor.json b/web/public/locales/zh-CN/views/configEditor.json index 79e9b398c..a4ca5c5b7 100644 --- a/web/public/locales/zh-CN/views/configEditor.json +++ b/web/public/locales/zh-CN/views/configEditor.json @@ -12,5 +12,7 @@ "savingError": "保存配置时出错" } }, - "confirm": "是否退出并不保存?" + "confirm": "是否退出并不保存?", + "safeConfigEditor": "配置编辑器(安全模式)", + "safeModeDescription": "由于验证配置出现错误,Frigate目前为安全模式。" } diff --git a/web/public/locales/zh-CN/views/events.json b/web/public/locales/zh-CN/views/events.json index 72a93104f..71d00848b 100644 --- a/web/public/locales/zh-CN/views/events.json +++ b/web/public/locales/zh-CN/views/events.json @@ -35,5 +35,7 @@ "selected": "已选择 {{count}} 个", "selected_one": "已选择 {{count}} 个", "selected_other": "已选择 {{count}} 个", - "detected": "已检测" + "detected": "已检测", + "suspiciousActivity": "可疑活动", + "threateningActivity": "威胁性活动" } diff --git a/web/public/locales/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json index 7db391e4d..bf53a9f8c 100644 --- a/web/public/locales/zh-CN/views/explore.json +++ b/web/public/locales/zh-CN/views/explore.json @@ -101,12 +101,14 @@ "success": { "regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。", "updatedSublabel": "成功更新子标签。", - "updatedLPR": "成功更新车牌。" + "updatedLPR": "成功更新车牌。", + "audioTranscription": "成功请求音频转录。" }, "error": { "regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}", "updatedSublabelFailed": "更新子标签失败:{{errorMessage}}", - "updatedLPRFailed": "更新车牌失败:{{errorMessage}}" + "updatedLPRFailed": "更新车牌失败:{{errorMessage}}", + "audioTranscription": "请求音频转录失败:{{errorMessage}}" } } }, @@ -152,6 +154,9 @@ "recognizedLicensePlate": "识别的车牌", "snapshotScore": { "label": "快照得分" + }, + "score": { + "label": "分值" } }, "itemMenu": { @@ -181,6 +186,14 @@ }, "deleteTrackedObject": { "label": "删除此跟踪对象" + }, + "addTrigger": { + "label": "添加触发器", + "aria": "为该追踪对象添加触发器" + }, + "audioTranscription": { + "label": "转录", + "aria": "请求音频转录" } }, "dialog": { @@ -201,5 +214,8 @@ }, "tooltip": "与 {{type}} 匹配度为 {{confidence}}%" }, - "exploreMore": "浏览更多的 {{label}}" + "exploreMore": "浏览更多的 {{label}}", + "aiAnalysis": { + "title": "AI分析" + } } diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json index 505781c4e..372c200e0 100644 --- a/web/public/locales/zh-CN/views/live.json +++ b/web/public/locales/zh-CN/views/live.json @@ -43,7 +43,15 @@ "label": "点击将PTZ摄像头画面居中" } }, - "presets": "PTZ摄像头预设" + "presets": "PTZ摄像头预设", + "focus": { + "in": { + "label": "PTZ摄像头聚焦" + }, + "out": { + "label": "PTZ摄像头拉远" + } + } }, "camera": { "enable": "开启摄像头", @@ -135,7 +143,8 @@ "recording": "录制", "snapshots": "快照", "audioDetection": "音频检测", - "autotracking": "自动跟踪" + "autotracking": "自动跟踪", + "transcription": "音频转录" }, "history": { "label": "显示历史录像" @@ -151,8 +160,12 @@ "editLayout": { "label": "编辑布局", "group": { - "label": "编辑摄像机分组" + "label": "编辑摄像头分组" }, "exitEdit": "退出编辑" + }, + "transcription": { + "enable": "启用实时音频转录", + "disable": "关闭实时音频转录" } } diff --git a/web/public/locales/zh-CN/views/search.json b/web/public/locales/zh-CN/views/search.json index b2f8c6d12..cb1c798de 100644 --- a/web/public/locales/zh-CN/views/search.json +++ b/web/public/locales/zh-CN/views/search.json @@ -12,7 +12,7 @@ "trackedObjectId": "跟踪对象 ID", "filter": { "label": { - "cameras": "摄像机", + "cameras": "摄像头", "labels": "标签", "zones": "区域", "sub_labels": "子标签", diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index fb92e6b7b..1d3d3abe8 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -178,6 +178,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": "摄像头名称为必填项", + "nameInvalid": "摄像头名称只能包含字母、数字、下划线或连字符", + "namePlaceholder": "比如:front_door", + "enabled": "开启", + "ffmpeg": { + "inputs": "视频流输入", + "path": "视频流路径", + "pathRequired": "视频流路径为必填项", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少需要指定一个功能", + "rolesUnique": "每个功能(音频、检测、录制)只能用于一个视频流,不能够重复分配到多个视频流", + "addInput": "添加视频流输入", + "removeInput": "移除视频流输入", + "inputsRequired": "至少需要一个视频流" + }, + "toast": { + "success": "摄像头 {{cameraName}} 保存已保存" + } } }, "masksAndZones": { @@ -417,6 +454,11 @@ "score": "分数", "ratio": "比例", "area": "区域" + }, + "paths": { + "title": "运动轨迹", + "desc": "显示被追踪对象运动轨迹的关键点", + "tips": "

运动轨迹

将使用线条和圆圈标示被追踪对象在其活动周期内移动的关键位置点。

" } }, "users": { @@ -677,5 +719,100 @@ }, "unsavedChanges": "增强功能设置未保存", "restart_required": "需要重启(增强功能设置已保存)" + }, + "triggers": { + "documentTitle": "触发器", + "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": "名称至少要两个字符。", + "invalidCharacters": "名称只能包含字母、数字、下划线和连字符。", + "alreadyExists": "此摄像头已存在同名触发器。" + } + }, + "enabled": { + "description": "开启/关闭此触发器" + }, + "type": { + "title": "类型", + "placeholder": "选择触发类型" + }, + "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}}" + } + } } } diff --git a/web/public/locales/zh-Hant/audio.json b/web/public/locales/zh-Hant/audio.json index bb37e6bd4..3d1f96b97 100644 --- a/web/public/locales/zh-Hant/audio.json +++ b/web/public/locales/zh-Hant/audio.json @@ -35,5 +35,29 @@ "vehicle": "車輛", "animal": "動物", "bark": "樹皮", - "goat": "山羊" + "goat": "山羊", + "whoop": "大叫", + "whispering": "講話", + "laughter": "笑聲", + "snicker": "竊笑", + "child_singing": "小孩歌聲", + "synthetic_singing": "合成音樂聲", + "rapping": "饒舌聲", + "humming": "哼歌聲", + "groan": "呻吟聲", + "grunt": "咕噥聲", + "whistling": "口哨聲", + "breathing": "呼吸聲", + "wheeze": "喘息聲", + "snoring": "打呼聲", + "gasp": "倒抽一口氣", + "pant": "喘氣聲", + "snort": "鼻息聲", + "cough": "咳嗽聲", + "throat_clearing": "清喉嚨聲", + "sneeze": "打噴嚏聲", + "sniff": "嗅聞聲", + "run": "跑步聲", + "shuffle": "拖著腳走路聲", + "footsteps": "腳步聲" } diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json index acc7a0a08..41659ba91 100644 --- a/web/public/locales/zh-Hant/common.json +++ b/web/public/locales/zh-Hant/common.json @@ -247,5 +247,6 @@ "title": "404", "desc": "找不到頁面" }, - "selectItem": "選擇 {{item}}" + "selectItem": "選擇 {{item}}", + "readTheDocumentation": "閱讀文件" } diff --git a/web/public/locales/zh-Hant/components/filter.json b/web/public/locales/zh-Hant/components/filter.json index a1192ac59..29ccaa5c2 100644 --- a/web/public/locales/zh-Hant/components/filter.json +++ b/web/public/locales/zh-Hant/components/filter.json @@ -122,5 +122,13 @@ "placeholder": "輸入以搜尋車牌…", "noLicensePlatesFound": "未找到車牌。", "selectPlatesFromList": "從列表中選擇一個或多個車牌。" + }, + "classes": { + "label": "類別", + "all": { + "title": "所有類別" + }, + "count_one": "{{count}} 個類別", + "count_other": "{{count}} 個類別" } } diff --git a/web/public/locales/zh-Hant/views/configEditor.json b/web/public/locales/zh-Hant/views/configEditor.json index 3788bace0..f1943edbb 100644 --- a/web/public/locales/zh-Hant/views/configEditor.json +++ b/web/public/locales/zh-Hant/views/configEditor.json @@ -12,5 +12,7 @@ } }, "saveOnly": "僅保存", - "confirm": "是否不保存就離開?" + "confirm": "是否不保存就離開?", + "safeConfigEditor": "設定編輯器(安全模式)", + "safeModeDescription": "由於設定驗證有誤,Frigate 進入安全模式。" } diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json index 8f840aab1..8571ea39f 100644 --- a/web/public/locales/zh-Hant/views/events.json +++ b/web/public/locales/zh-Hant/views/events.json @@ -34,5 +34,7 @@ "selected_one": "已選擇 {{count}} 個", "selected_other": "已選擇 {{count}} 個", "camera": "鏡頭", - "detected": "已偵測" + "detected": "已偵測", + "suspiciousActivity": "可疑的活動", + "threateningActivity": "有威脅性的活動" } diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json index 55947b9f2..89163c677 100644 --- a/web/public/locales/zh-Hant/views/live.json +++ b/web/public/locales/zh-Hant/views/live.json @@ -39,7 +39,15 @@ "label": "點擊畫面以置中 PTZ 鏡頭" } }, - "presets": "PTZ 鏡頭預設" + "presets": "PTZ 鏡頭預設", + "focus": { + "in": { + "label": "聚焦 PTZ 鏡頭" + }, + "out": { + "label": "離焦 PTZ 鏡頭" + } + } }, "cameraAudio": { "enable": "啟用鏡頭音訊", @@ -154,5 +162,9 @@ "label": "編輯鏡頭群組" }, "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時語音轉錄", + "disable": "停用即時語音轉錄" } } diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json index d252250e9..2dd0dd9e7 100644 --- a/web/public/locales/zh-Hant/views/settings.json +++ b/web/public/locales/zh-Hant/views/settings.json @@ -39,7 +39,51 @@ "automaticLiveView": { "label": "自動即時檢視", "desc": "在偵測到移動時自動切換至即時影像。停用此設定將使得在即時監控面板上的靜態畫面每分鐘更新一次。" + }, + "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/src/api/ws.tsx b/web/src/api/ws.tsx index 0cef235a0..8ebc01727 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -10,6 +10,7 @@ import { ToggleableSetting, TrackedObjectUpdateReturnType, TriggerStatus, + FrigateAudioDetections, } from "@/types/ws"; import { FrigateStats } from "@/types/stats"; import { createContainer } from "react-tracked"; @@ -341,6 +342,13 @@ export function useFrigateEvents(): { payload: FrigateEvent } { return { payload: JSON.parse(payload as string) }; } +export function useAudioDetections(): { payload: FrigateAudioDetections } { + const { + value: { payload }, + } = useWs("audio_detections", ""); + return { payload: JSON.parse(payload as string) }; +} + export function useFrigateReviews(): FrigateReview { const { value: { payload }, diff --git a/web/src/components/audio/AudioLevelGraph.tsx b/web/src/components/audio/AudioLevelGraph.tsx new file mode 100644 index 000000000..4f0e75722 --- /dev/null +++ b/web/src/components/audio/AudioLevelGraph.tsx @@ -0,0 +1,165 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { MdCircle } from "react-icons/md"; +import Chart from "react-apexcharts"; +import { useTheme } from "@/context/theme-provider"; +import { useWs } from "@/api/ws"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; + +const GRAPH_COLORS = ["#3b82f6", "#ef4444"]; // RMS, dBFS + +interface AudioLevelGraphProps { + cameraName: string; +} + +export function AudioLevelGraph({ cameraName }: AudioLevelGraphProps) { + const [audioData, setAudioData] = useState< + { timestamp: number; rms: number; dBFS: number }[] + >([]); + const [maxDataPoints] = useState(50); + + // config for time formatting + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const locale = useDateLocale(); + const { t } = useTranslation(["common"]); + + const { + value: { payload: audioRms }, + } = useWs(`${cameraName}/audio/rms`, ""); + const { + value: { payload: audioDBFS }, + } = useWs(`${cameraName}/audio/dBFS`, ""); + + useEffect(() => { + if (typeof audioRms === "number") { + const now = Date.now(); + setAudioData((prev) => { + const next = [ + ...prev, + { + timestamp: now, + rms: audioRms, + dBFS: typeof audioDBFS === "number" ? audioDBFS : 0, + }, + ]; + return next.slice(-maxDataPoints); + }); + } + }, [audioRms, audioDBFS, maxDataPoints]); + + const series = useMemo( + () => [ + { + name: "RMS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.rms })), + }, + { + name: "dBFS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.dBFS })), + }, + ], + [audioData], + ); + + const lastValues = useMemo(() => { + if (!audioData.length) return undefined; + const last = audioData[audioData.length - 1]; + return [last.rms, last.dBFS]; + }, [audioData]); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const formatString = useMemo( + () => + t(`time.formattedTimestampHourMinuteSecond.${timeFormat}`, { + ns: "common", + }), + [t, timeFormat], + ); + + const formatTime = useCallback( + (val: unknown) => { + const seconds = Math.round(Number(val) / 1000); + return formatUnixTimestampToDateTime(seconds, { + timezone: config?.ui.timezone, + date_format: formatString, + locale, + }); + }, + [config?.ui.timezone, formatString, locale], + ); + + const { theme, systemTheme } = useTheme(); + + const options = useMemo(() => { + return { + chart: { + id: `${cameraName}-audio`, + selection: { enabled: false }, + toolbar: { show: false }, + zoom: { enabled: false }, + animations: { enabled: false }, + }, + colors: GRAPH_COLORS, + grid: { + show: true, + borderColor: "#374151", + strokeDashArray: 3, + xaxis: { lines: { show: true } }, + yaxis: { lines: { show: true } }, + }, + legend: { show: false }, + dataLabels: { enabled: false }, + stroke: { width: 1 }, + markers: { size: 0 }, + tooltip: { + theme: systemTheme || theme, + x: { formatter: (val: number) => formatTime(val) }, + y: { formatter: (v: number) => v.toFixed(1) }, + }, + xaxis: { + type: "datetime", + labels: { + rotate: 0, + formatter: formatTime, + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + axisBorder: { show: false }, + axisTicks: { show: false }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.round(val).toString(), + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + }, + } as ApexCharts.ApexOptions; + }, [cameraName, theme, systemTheme, formatTime]); + + return ( +
+ {lastValues && ( +
+ {["RMS", "dBFS"].map((label, idx) => ( +
+ +
{label}
+
+ {lastValues[idx].toFixed(1)} +
+
+ ))} +
+ )} + +
+ ); +} diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index c4ccdff84..f747f75ab 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -65,6 +65,7 @@ import { useTranslation } from "react-i18next"; import { supportedLanguageKeys } from "@/lib/const"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { MdCategory } from "react-icons/md"; type GeneralSettingsProps = { className?: string; @@ -315,6 +316,19 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { )} + {isAdmin && isMobile && ( + <> + + + + {t("menu.classification")} + + + + )} {t("menu.appearance")} diff --git a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx index f71c2d6ba..9e92bc011 100644 --- a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx +++ b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx @@ -187,9 +187,7 @@ export function AnnotationSettingsPane({ rel="noopener noreferrer" className="inline" > - {t( - "objectLifecycle.annotationSettings.offset.documentation", - )} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx b/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx index 9207ce02f..3ed512303 100644 --- a/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx +++ b/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx @@ -161,7 +161,7 @@ export default function CreateFaceWizardDialog({ rel="noopener noreferrer" className="inline" > - {t("readTheDocs")} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index cf562d598..63755c738 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -87,10 +87,10 @@ export default function ReviewDetailDialog({ let concerns = ""; switch (aiAnalysis.potential_threat_level) { case ThreatLevel.SUSPICIOUS: - concerns = "• Suspicious Activity\n"; + concerns = `• ${t("suspiciousActivity", { ns: "views/events" })}\n`; break; case ThreatLevel.DANGER: - concerns = "• Danger\n"; + concerns = `• ${t("threateningActivity", { ns: "views/events" })}\n`; break; } @@ -99,7 +99,7 @@ export default function ReviewDetailDialog({ }); return concerns || "None"; - }, [aiAnalysis]); + }, [aiAnalysis, t]); const hasMismatch = useMemo(() => { if (!review || !events) { @@ -271,12 +271,18 @@ export default function ReviewDetailDialog({ isDesktop && "m-2 w-[90%]", )} > - AI Analysis -
Description
+ {t("aiAnalysis.title")} +
+ {t("details.description.label")} +
{aiAnalysis.scene}
-
Score
+
+ {t("details.score.label")} +
{aiAnalysis.confidence * 100}%
-
Concerns
+
+ {t("concerns.label")} +
{aiThreatLevel}
)} diff --git a/web/src/components/overlay/dialog/TrainFilterDialog.tsx b/web/src/components/overlay/dialog/TrainFilterDialog.tsx index 56037ec0a..f4ccf41e1 100644 --- a/web/src/components/overlay/dialog/TrainFilterDialog.tsx +++ b/web/src/components/overlay/dialog/TrainFilterDialog.tsx @@ -60,7 +60,7 @@ export default function TrainFilterDialog({ moreFiltersSelected ? "text-white" : "text-secondary-foreground", )} /> - {t("more")} + {isDesktop && t("more")} ); const content = ( diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 0e66b3c26..9d84a6697 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -22,6 +22,8 @@ import { InProgressPreview, VideoPreview } from "../preview/ScrubbablePreview"; import { Preview } from "@/types/preview"; import { baseUrl } from "@/api/baseUrl"; import { useTranslation } from "react-i18next"; +import { FaExclamationTriangle } from "react-icons/fa"; +import { MdOutlinePersonSearch } from "react-icons/md"; type PreviewPlayerProps = { review: ReviewSegment; @@ -234,7 +236,12 @@ export default function PreviewThumbnailPlayer({ )} /> )} -
+
setTooltipHovering(false)} > -
+
{(review.severity == "alert" || review.severity == "detection") && ( <> @@ -279,6 +286,45 @@ export default function PreviewThumbnailPlayer({ .replaceAll("-verified", "")} + {!!( + review.data.metadata?.potential_threat_level && + !review.has_been_reviewed + ) && ( + +
setTooltipHovering(true)} + onMouseLeave={() => setTooltipHovering(false)} + > + +
+ {(review.severity == "alert" || + review.severity == "detection") && ( + <> + onClick(review, false, true)} + > + {review.data.metadata.potential_threat_level == 1 ? ( + + ) : ( + + )} + + + )} +
+
+
+ + {review.data.metadata.potential_threat_level == 1 ? ( + <>{t("suspiciousActivity", { ns: "views/events" })} + ) : ( + <>{t("threateningActivity", { ns: "views/events" })} + )} + +
+ )}
{!playingBack && (
- {t("streaming.restreaming.desc.readTheDocumentation", { - ns: "components/dialog", - })} + {t("readTheDocumentation", { ns: "common" })}
@@ -292,7 +290,7 @@ export function CameraStreamingDialog({ rel="noopener noreferrer" className="inline" > - {t("group.camera.setting.audio.tips.document")} + {t("readTheDocumentation", { ns: "common" })}
diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index b59005d0b..ddb78c877 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -257,7 +257,7 @@ export default function MotionMaskEditPane({ rel="noopener noreferrer" className="inline" > - {t("masksAndZones.motionMasks.context.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })}
@@ -303,7 +303,7 @@ export default function MotionMaskEditPane({ rel="noopener noreferrer" className="my-3 block" > - {t("masksAndZones.motionMasks.polygonAreaTooLarge.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })}{" "}
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 17029728b..67f5f690a 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -687,7 +687,7 @@ export default function ZoneEditPane({ rel="noopener noreferrer" className="inline" > - {t("masksAndZones.zones.speedEstimation.docs")} + {t("readTheDocumentation", { ns: "common" })}
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index b81ad54b0..328811a9d 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -1,4 +1,5 @@ import { + useAudioDetections, useEnabledState, useFrigateEvents, useInitialCameraState, @@ -8,7 +9,7 @@ import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import { MotionData, ReviewSegment } from "@/types/review"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTimelineUtils } from "./use-timeline-utils"; -import { ObjectType } from "@/types/ws"; +import { AudioDetection, ObjectType } from "@/types/ws"; import useDeepMemo from "./use-deep-memo"; import { isEqual } from "lodash"; import { useAutoFrigateStats } from "./use-stats"; @@ -20,6 +21,7 @@ type useCameraActivityReturn = { activeTracking: boolean; activeMotion: boolean; objects: ObjectType[]; + audio_detections: AudioDetection[]; offline: boolean; }; @@ -38,6 +40,9 @@ export function useCameraActivity( return getAttributeLabels(config); }, [config]); const [objects, setObjects] = useState([]); + const [audioDetections, setAudioDetections] = useState< + AudioDetection[] | undefined + >([]); // init camera activity @@ -51,6 +56,15 @@ export function useCameraActivity( } }, [updatedCameraState, camera]); + const { payload: updatedAudioState } = useAudioDetections(); + const memoizedAudioState = useDeepMemo(updatedAudioState); + + useEffect(() => { + if (memoizedAudioState) { + setAudioDetections(memoizedAudioState[camera.name]); + } + }, [memoizedAudioState, camera]); + // handle camera activity const hasActiveObjects = useMemo( @@ -144,7 +158,9 @@ export function useCameraActivity( return false; } - return cameras[camera.name].camera_fps == 0 && stats["service"].uptime > 60; + return ( + cameras[camera.name]?.camera_fps == 0 && stats["service"].uptime > 60 + ); }, [camera, stats]); const isCameraEnabled = cameraEnabled ? cameraEnabled === "ON" : true; @@ -158,6 +174,7 @@ export function useCameraActivity( : updatedCameraState?.motion === true : false, objects: isCameraEnabled ? (objects ?? []) : [], + audio_detections: isCameraEnabled ? (audioDetections ?? []) : [], offline, }; } diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index b6a4a43e5..9acc066ba 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -482,9 +482,7 @@ export default function Explore() { rel="noopener noreferrer" className="inline" > - {t( - "exploreIsUnavailable.downloadingModels.tips.documentation", - )}{" "} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index a196dff18..c98ebe80f 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -79,6 +79,7 @@ export type StorageStats = { total: number; used: number; mount_type: string; + min_shm?: number; }; export type PotentialProblem = { diff --git a/web/src/types/ws.ts b/web/src/types/ws.ts index f2e45bda4..3a464a275 100644 --- a/web/src/types/ws.ts +++ b/web/src/types/ws.ts @@ -51,6 +51,12 @@ export type ObjectType = { sub_label: string; }; +export type AudioDetection = { + id: string; + label: string; + score: number; +}; + export interface FrigateCameraState { config: { enabled: boolean; @@ -69,6 +75,10 @@ export interface FrigateCameraState { }; motion: boolean; objects: ObjectType[]; + audio_detections: AudioDetection[]; +} +export interface FrigateAudioDetections { + [camera: string]: AudioDetection[]; } export type ModelState = diff --git a/web/src/views/classification/ModelSelectionView.tsx b/web/src/views/classification/ModelSelectionView.tsx index aa2f94c6a..2dd7e0375 100644 --- a/web/src/views/classification/ModelSelectionView.tsx +++ b/web/src/views/classification/ModelSelectionView.tsx @@ -79,7 +79,9 @@ function ModelCard({ config, onClick }: ModelCardProps) { )} onClick={() => onClick()} > -
+
{Object.entries(coverImages).map(([key, image]) => ( )} - {t("button.trainModel")} + {isDesktop && t("button.trainModel")}
)} @@ -713,7 +713,7 @@ function TrainGrid({ if ( trainFilter.max_score && - trainFilter.max_score <= data.score / 100.0 + trainFilter.max_score < data.score / 100.0 ) { return false; } @@ -725,7 +725,12 @@ function TrainGrid({ ); return ( -
+
{trainData?.map((data) => (
{ e.stopPropagation(); diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 69d4a26f4..b4d099dd7 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -1379,12 +1379,7 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t( - "streaming.restreaming.desc.readTheDocumentation", - { - ns: "components/dialog", - }, - )} + {t("readTheDocumentation", { ns: "common" })}
@@ -1457,7 +1452,9 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t("stream.audio.tips.documentation")} + {t("readTheDocumentation", { + ns: "common", + })}
@@ -1500,9 +1497,9 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t( - "stream.twoWayTalk.tips.documentation", - )} + {t("readTheDocumentation", { + ns: "common", + })}
@@ -1717,9 +1714,7 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t("streaming.restreaming.desc.readTheDocumentation", { - ns: "components/dialog", - })} + {t("readTheDocumentation", { ns: "common" })}
@@ -1788,7 +1783,7 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t("stream.audio.tips.documentation")} + {t("readTheDocumentation", { ns: "common" })} @@ -1831,7 +1826,7 @@ function FrigateCameraFeatures({ rel="noopener noreferrer" className="inline" > - {t("stream.twoWayTalk.tips.documentation")} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/CameraSettingsView.tsx b/web/src/views/settings/CameraSettingsView.tsx index 90344b470..8894a5b8f 100644 --- a/web/src/views/settings/CameraSettingsView.tsx +++ b/web/src/views/settings/CameraSettingsView.tsx @@ -514,9 +514,7 @@ export default function CameraSettingsView({ rel="noopener noreferrer" className="inline" > - - camera.reviewClassification.readTheDocumentation - {" "} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/EnrichmentsSettingsView.tsx b/web/src/views/settings/EnrichmentsSettingsView.tsx index 2f00b3f2c..a2a835969 100644 --- a/web/src/views/settings/EnrichmentsSettingsView.tsx +++ b/web/src/views/settings/EnrichmentsSettingsView.tsx @@ -263,7 +263,7 @@ export default function EnrichmentsSettingsView({ rel="noopener noreferrer" className="inline" > - {t("enrichments.semanticSearch.readTheDocumentation")} + {t("readTheDocumentation", { ns: "common" })} @@ -409,7 +409,7 @@ export default function EnrichmentsSettingsView({ rel="noopener noreferrer" className="inline" > - {t("enrichments.faceRecognition.readTheDocumentation")} + {t("readTheDocumentation", { ns: "common" })} @@ -512,9 +512,7 @@ export default function EnrichmentsSettingsView({ rel="noopener noreferrer" className="inline" > - {t( - "enrichments.licensePlateRecognition.readTheDocumentation", - )} + {t("readTheDocumentation", { ns: "common" })} @@ -558,7 +556,7 @@ export default function EnrichmentsSettingsView({ rel="noopener noreferrer" className="inline" > - {t("enrichments.semanticSearch.readTheDocumentation")} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 5a322ca06..39d48f02c 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -459,7 +459,7 @@ export default function FrigatePlusSettingsView({ rel="noopener noreferrer" className="inline" > - {t("frigatePlus.snapshotConfig.documentation")} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/MasksAndZonesView.tsx b/web/src/views/settings/MasksAndZonesView.tsx index c9ba9971e..320fe0994 100644 --- a/web/src/views/settings/MasksAndZonesView.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -506,7 +506,7 @@ export default function MasksAndZonesView({ rel="noopener noreferrer" className="inline" > - {t("masksAndZones.zones.desc.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })} @@ -574,9 +574,7 @@ export default function MasksAndZonesView({ rel="noopener noreferrer" className="inline" > - {t( - "masksAndZones.motionMasks.desc.documentation", - )}{" "} + {t("readTheDocumentation", { ns: "common" })} @@ -646,9 +644,7 @@ export default function MasksAndZonesView({ rel="noopener noreferrer" className="inline" > - {t( - "masksAndZones.objectMasks.desc.documentation", - )}{" "} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/MotionTunerView.tsx b/web/src/views/settings/MotionTunerView.tsx index ec75298eb..152de774f 100644 --- a/web/src/views/settings/MotionTunerView.tsx +++ b/web/src/views/settings/MotionTunerView.tsx @@ -205,7 +205,7 @@ export default function MotionTunerView({ rel="noopener noreferrer" className="inline" > - {t("motionDetectionTuner.desc.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })} diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index 6742a09f8..3b2c89f05 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -341,7 +341,7 @@ export default function NotificationView({ rel="noopener noreferrer" className="inline" > - {t("notification.notificationSettings.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })} @@ -363,7 +363,7 @@ export default function NotificationView({ rel="noopener noreferrer" className="inline" > - {t("notification.notificationUnavailable.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })}{" "} @@ -396,7 +396,7 @@ export default function NotificationView({ rel="noopener noreferrer" className="inline" > - {t("notification.notificationSettings.documentation")}{" "} + {t("readTheDocumentation", { ns: "common" })}{" "} diff --git a/web/src/views/settings/ObjectSettingsView.tsx b/web/src/views/settings/ObjectSettingsView.tsx index 3572f55c4..6c6363f9d 100644 --- a/web/src/views/settings/ObjectSettingsView.tsx +++ b/web/src/views/settings/ObjectSettingsView.tsx @@ -16,7 +16,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { ObjectType } from "@/types/ws"; +import { AudioDetection, ObjectType } from "@/types/ws"; import useDeepMemo from "@/hooks/use-deep-memo"; import { Card } from "@/components/ui/card"; import { getIconForLabel } from "@/utils/iconUtil"; @@ -31,6 +31,8 @@ import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { getTranslatedLabel } from "@/utils/i18n"; import { useCameraNickname } from "@/hooks/use-camera-nickname"; +import { AudioLevelGraph } from "@/components/audio/AudioLevelGraph"; +import { useWs } from "@/api/ws"; type ObjectSettingsViewProps = { selectedCamera?: string; @@ -129,9 +131,12 @@ export default function ObjectSettingsView({ const cameraName = useCameraNickname(cameraConfig); - const { objects } = useCameraActivity(cameraConfig ?? ({} as CameraConfig)); + const { objects, audio_detections } = useCameraActivity( + cameraConfig ?? ({} as CameraConfig), + ); const memoizedObjects = useDeepMemo(objects); + const memoizedAudio = useDeepMemo(audio_detections); const searchParams = useMemo(() => { if (!optionsLoaded) { @@ -194,11 +199,18 @@ export default function ObjectSettingsView({ )} - + input.roles.includes("audio")) ? "grid-cols-3" : "grid-cols-2"}`} + > {t("debug.debugging")} {t("debug.objectList")} + {cameraConfig.ffmpeg.inputs.some((input) => + input.roles.includes("audio"), + ) && ( + {t("debug.audio.title")} + )}
@@ -280,7 +292,7 @@ export default function ObjectSettingsView({ rel="noopener noreferrer" className="inline" > - {t("debug.objectShapeFilterDrawing.document")} + {t("readTheDocumentation", { ns: "common" })}
@@ -309,6 +321,16 @@ export default function ObjectSettingsView({ + {cameraConfig.ffmpeg.inputs.some((input) => + input.roles.includes("audio"), + ) && ( + + + + )}
@@ -367,7 +389,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) { return (
{objects && objects.length > 0 ? ( - objects.map((obj) => { + objects.map((obj: ObjectType) => { return (
@@ -443,3 +465,61 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
); } + +type AudioListProps = { + cameraConfig: CameraConfig; + audioDetections?: AudioDetection[]; +}; + +function AudioList({ cameraConfig, audioDetections }: AudioListProps) { + const { t } = useTranslation(["views/settings"]); + + // Get audio levels directly from ws hooks + const { + value: { payload: audioRms }, + } = useWs(`${cameraConfig.name}/audio/rms`, ""); + const { + value: { payload: audioDBFS }, + } = useWs(`${cameraConfig.name}/audio/dBFS`, ""); + + return ( +
+ {audioDetections && Object.keys(audioDetections).length > 0 ? ( + Object.entries(audioDetections).map(([key, obj]) => ( + +
+
+
+ {getIconForLabel(key, "size-5 text-white")} +
+
{getTranslatedLabel(key)}
+
+
+
+
+

+ {t("debug.audio.score")} +

+ {obj.score ? (obj.score * 100).toFixed(1).toString() : "-"}% +
+
+
+
+
+ )) + ) : ( +
+

{t("debug.audio.noAudioDetections")}

+

+ {t("debug.audio.currentRMS")}{" "} + {(typeof audioRms === "number" ? audioRms : 0).toFixed(1)} |{" "} + {t("debug.audio.currentdbFS")}{" "} + {(typeof audioDBFS === "number" ? audioDBFS : 0).toFixed(1)} +

+
+ )} + + +
+ ); +} diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx index 1da8cc94b..cd23c339b 100644 --- a/web/src/views/system/CameraMetrics.tsx +++ b/web/src/views/system/CameraMetrics.tsx @@ -39,7 +39,12 @@ export default function CameraMetrics({ // stats const { data: initialStats } = useSWR( - ["stats/history", { keys: "cpu_usages,cameras,detection_fps,service" }], + [ + "stats/history", + { + keys: "cpu_usages,cameras,camera_fps,detection_fps,skipped_fps,service", + }, + ], { revalidateOnFocus: false, }, diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx index 6ae40089a..b91e66986 100644 --- a/web/src/views/system/StorageMetrics.tsx +++ b/web/src/views/system/StorageMetrics.tsx @@ -14,6 +14,10 @@ import { useFormattedTimestamp, useTimezone } from "@/hooks/use-date-utils"; import { RecordingsSummary } from "@/types/review"; import { useTranslation } from "react-i18next"; import { TZDate } from "react-day-picker"; +import { Link } from "react-router-dom"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { LuExternalLink } from "react-icons/lu"; +import { FaExclamationTriangle } from "react-icons/fa"; type CameraStorage = { [key: string]: { @@ -36,6 +40,7 @@ export default function StorageMetrics({ }); const { t } = useTranslation(["views/system"]); const timezone = useTimezone(config); + const { getLocaleDocUrl } = useDocDomain(); const totalStorage = useMemo(() => { if (!cameraStorage || !stats) { @@ -142,7 +147,46 @@ export default function StorageMetrics({ />
-
/dev/shm
+
+ /dev/shm + {stats.service.storage["/dev/shm"]["total"] < + (stats.service.storage["/dev/shm"]["min_shm"] ?? 0) && ( + + + + + +
+ {t("storage.shm.warning", { + total: stats.service.storage["/dev/shm"]["total"], + min_shm: stats.service.storage["/dev/shm"]["min_shm"], + })} +
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+
+
+ )} +